1use std::{path::PathBuf, time::Instant};
2
3use fallow_config::WorkspaceInfo;
4use fallow_engine::{
5 dead_code::DeadCodeAnalysisArtifacts,
6 project_analysis::{ProjectAnalysisArtifactOptions, ProjectAnalysisArtifacts},
7 session::AnalysisSession,
8};
9use fallow_output::{CombinedNextStepsInput, build_combined_next_steps};
10use rustc_hash::FxHashSet;
11
12use crate::{
13 AnalysisOptions, CombinedOptions, CombinedProgrammaticOutput, ComplexityOptions,
14 DeadCodeFilters, DeadCodeOptions, DuplicationOptions, ProgrammaticError,
15 analysis_context::{
16 changed_files_for_run, resolve_programmatic_analysis_context_deferred_workspace,
17 },
18 next_steps::{
19 default_workspace_ref, default_workspace_ref_for_workspaces, setup_pointer_applicable,
20 suggestions_enabled,
21 },
22};
23
24use super::{
25 EffectiveProductionModes, ProgrammaticResult, health_may_consume_dead_code_artifacts,
26 health_may_consume_duplication_report, resolve_effective_production_modes, root_envelope_mode,
27 run_duplication, run_health, run_health_with_session_artifacts,
28};
29
30struct PreparedCombinedOptions {
31 dead_code: DeadCodeOptions,
32 duplication: DuplicationOptions,
33 health: ComplexityOptions,
34}
35
36struct CombinedSectionRun {
37 dead_code: Option<crate::DeadCodeProgrammaticOutput>,
38 duplication: Option<crate::DuplicationProgrammaticOutput>,
39 health: Option<crate::HealthProgrammaticOutput>,
40 root: PathBuf,
41 workspaces: Option<Vec<WorkspaceInfo>>,
42}
43
44struct DeadCodeSessionRun<'a> {
45 options: &'a CombinedOptions,
46 resolved: &'a crate::analysis_context::ProgrammaticAnalysisContext,
47 prepared: &'a PreparedCombinedOptions,
48 changed_files: Option<&'a FxHashSet<PathBuf>>,
49 session: &'a AnalysisSession,
50}
51
52pub fn run_combined(options: &CombinedOptions) -> ProgrammaticResult<CombinedProgrammaticOutput> {
59 if !(options.dead_code || options.duplication || options.health) {
60 return Err(ProgrammaticError::new(
61 "combined analysis requires at least one enabled section",
62 2,
63 )
64 .with_code("FALLOW_COMBINED_EMPTY")
65 .with_context("combined"));
66 }
67
68 let start = Instant::now();
69 let resolved = resolve_programmatic_analysis_context_deferred_workspace(&options.analysis)?;
70 resolved.install(|| {
71 let production_modes = resolve_effective_production_modes(&resolved, None, None, None)?;
72 let prepared = prepare_combined_options(options, production_modes);
73 let changed_files = changed_files_for_run(&resolved)?;
74 let sections = run_combined_sections(
75 options,
76 &resolved,
77 &prepared,
78 changed_files.as_ref(),
79 production_modes,
80 )?;
81
82 let next_steps = combined_next_steps(
83 sections.dead_code.as_ref(),
84 sections.duplication.as_ref(),
85 sections.health.as_ref(),
86 §ions.root,
87 sections.workspaces.as_deref(),
88 );
89
90 Ok(CombinedProgrammaticOutput {
91 dead_code: sections.dead_code,
92 duplication: sections.duplication,
93 health: sections.health,
94 root: sections.root,
95 elapsed: start.elapsed(),
96 explain: options.analysis.explain,
97 next_steps,
98 envelope_mode: root_envelope_mode(),
99 telemetry_analysis_run_id: None,
100 })
101 })
102}
103
104fn prepare_combined_options(
105 options: &CombinedOptions,
106 production_modes: EffectiveProductionModes,
107) -> PreparedCombinedOptions {
108 PreparedCombinedOptions {
109 dead_code: combined_dead_code_options(options, production_modes.dead_code),
110 duplication: combined_duplication_options(options, production_modes.dupes),
111 health: combined_health_options(options, production_modes.health),
112 }
113}
114
115fn run_combined_sections(
116 options: &CombinedOptions,
117 resolved: &crate::analysis_context::ProgrammaticAnalysisContext,
118 prepared: &PreparedCombinedOptions,
119 changed_files: Option<&FxHashSet<PathBuf>>,
120 production_modes: EffectiveProductionModes,
121) -> ProgrammaticResult<CombinedSectionRun> {
122 let share_health = options.dead_code
123 && options.health
124 && production_modes.dead_code == production_modes.health;
125 let share_dupes = options.dead_code
126 && options.duplication
127 && production_modes.dead_code == production_modes.dupes;
128 if share_health || share_dupes {
129 return run_combined_with_dead_code_session(
130 options,
131 resolved,
132 prepared,
133 changed_files,
134 share_health,
135 share_dupes,
136 );
137 }
138 run_combined_sections_isolated(options, resolved, prepared)
139}
140
141fn run_combined_with_dead_code_session(
142 options: &CombinedOptions,
143 resolved: &crate::analysis_context::ProgrammaticAnalysisContext,
144 prepared: &PreparedCombinedOptions,
145 changed_files: Option<&FxHashSet<PathBuf>>,
146 share_health: bool,
147 share_dupes: bool,
148) -> ProgrammaticResult<CombinedSectionRun> {
149 let session = super::dead_code::load_dead_code_session(&prepared.dead_code, resolved)?;
150 if share_dupes {
151 return run_combined_with_project_artifacts(CombinedProjectArtifactRun {
152 options,
153 resolved,
154 prepared,
155 changed_files,
156 share_health,
157 session: &session,
158 });
159 }
160 let ctx = DeadCodeSessionRun {
161 options,
162 resolved,
163 prepared,
164 changed_files,
165 session: &session,
166 };
167 let (dead_code, dead_code_artifacts) =
168 run_dead_code_with_optional_artifacts(&ctx, options.health && share_health)?;
169 let duplication = run_combined_duplication(&ctx, share_dupes)?;
170 let health = run_combined_health(&ctx, share_health, dead_code_artifacts, None)?;
171 Ok(CombinedSectionRun {
172 dead_code,
173 duplication,
174 health,
175 root: session.root().to_path_buf(),
176 workspaces: Some(session.workspaces().to_vec()),
177 })
178}
179
180#[derive(Clone, Copy)]
181struct CombinedProjectArtifactRun<'a> {
182 options: &'a CombinedOptions,
183 resolved: &'a crate::analysis_context::ProgrammaticAnalysisContext,
184 prepared: &'a PreparedCombinedOptions,
185 changed_files: Option<&'a FxHashSet<PathBuf>>,
186 share_health: bool,
187 session: &'a AnalysisSession,
188}
189
190fn run_combined_with_project_artifacts(
191 run: CombinedProjectArtifactRun<'_>,
192) -> ProgrammaticResult<CombinedSectionRun> {
193 let CombinedProjectArtifactRun {
194 options,
195 resolved,
196 prepared,
197 changed_files,
198 share_health,
199 session,
200 } = run;
201 let retain_dead_code_artifacts =
202 share_health && health_may_consume_dead_code_artifacts(&prepared.health, session.config());
203 let section_start = Instant::now();
204 let project = analyze_project_artifacts_for_combined(&run, retain_dead_code_artifacts)?;
205 let dead_code = super::dead_code::run_dead_code_from_artifacts(
206 &prepared.dead_code,
207 resolved,
208 session,
209 changed_files,
210 project.dead_code,
211 section_start,
212 )?;
213 let pre_computed_duplication_for_health =
214 should_precompute_duplication_for_combined_health(options, prepared, share_health)
215 .then(|| project.duplication.clone());
216 let duplication = run_project_artifact_duplication(
217 options,
218 prepared,
219 resolved,
220 session,
221 project.duplication,
222 section_start,
223 )?;
224 let super::dead_code::DeadCodeProgrammaticRunWithArtifacts {
225 output: dead_code,
226 artifacts,
227 } = dead_code;
228 let dead_code_artifacts = retain_dead_code_artifacts.then_some(artifacts);
229 let health = run_combined_health(
230 &DeadCodeSessionRun {
231 options,
232 resolved,
233 prepared,
234 changed_files,
235 session,
236 },
237 share_health,
238 dead_code_artifacts,
239 pre_computed_duplication_for_health,
240 )?;
241
242 Ok(CombinedSectionRun {
243 dead_code: Some(dead_code),
244 duplication,
245 health,
246 root: session.root().to_path_buf(),
247 workspaces: Some(session.workspaces().to_vec()),
248 })
249}
250
251fn run_project_artifact_duplication(
252 options: &CombinedOptions,
253 prepared: &PreparedCombinedOptions,
254 resolved: &crate::analysis_context::ProgrammaticAnalysisContext,
255 session: &AnalysisSession,
256 duplication: fallow_engine::duplicates::DuplicationReport,
257 section_start: Instant,
258) -> ProgrammaticResult<Option<crate::DuplicationProgrammaticOutput>> {
259 options
260 .duplication
261 .then(|| {
262 super::duplication::run_duplication_report_with_session(
263 &prepared.duplication,
264 resolved,
265 session,
266 duplication,
267 section_start,
268 )
269 })
270 .transpose()
271}
272
273fn should_precompute_duplication_for_combined_health(
274 options: &CombinedOptions,
275 prepared: &PreparedCombinedOptions,
276 share_health: bool,
277) -> bool {
278 options.health
279 && share_health
280 && health_may_consume_duplication_report(&prepared.health)
281 && duplication_options_preserve_health_config(&prepared.duplication)
282}
283
284fn analyze_project_artifacts_for_combined(
285 run: &CombinedProjectArtifactRun<'_>,
286 retain_dead_code_artifacts: bool,
287) -> ProgrammaticResult<ProjectAnalysisArtifacts> {
288 let dupes_config = super::duplication::build_dupes_config(
289 &run.prepared.duplication,
290 &run.session.config().duplicates,
291 );
292 run.session
293 .analyze_project_with_artifacts(
294 &dupes_config,
295 ProjectAnalysisArtifactOptions {
296 retain_complexity_artifacts: retain_dead_code_artifacts,
297 retain_graph: retain_dead_code_artifacts,
298 changed_files: run.changed_files.cloned(),
299 collect_source_fingerprints: false,
300 },
301 )
302 .map_err(|err| {
303 ProgrammaticError::new(format!("combined analysis failed: {err}"), 2)
304 .with_code("FALLOW_COMBINED_FAILED")
305 .with_context("combined")
306 })
307}
308
309fn run_dead_code_with_optional_artifacts(
310 ctx: &DeadCodeSessionRun<'_>,
311 share_health: bool,
312) -> ProgrammaticResult<(
313 Option<crate::DeadCodeProgrammaticOutput>,
314 Option<DeadCodeAnalysisArtifacts>,
315)> {
316 let retain_artifacts = share_health
317 && health_may_consume_dead_code_artifacts(&ctx.prepared.health, ctx.session.config());
318 if retain_artifacts {
319 let dead_code = super::dead_code::run_dead_code_with_session_artifacts(
320 &ctx.prepared.dead_code,
321 ctx.resolved,
322 ctx.session,
323 ctx.changed_files,
324 |_| {},
325 Instant::now(),
326 )?;
327 return Ok((Some(dead_code.output), Some(dead_code.artifacts)));
328 }
329 let dead_code = super::dead_code::run_dead_code_with_session(
330 &ctx.prepared.dead_code,
331 ctx.resolved,
332 ctx.session,
333 ctx.changed_files,
334 |_| {},
335 Instant::now(),
336 )?;
337 Ok((Some(dead_code), None))
338}
339
340fn run_combined_duplication(
341 ctx: &DeadCodeSessionRun<'_>,
342 share_dupes: bool,
343) -> ProgrammaticResult<Option<crate::DuplicationProgrammaticOutput>> {
344 if !ctx.options.duplication {
345 return Ok(None);
346 }
347 if !share_dupes {
348 return run_duplication(&ctx.prepared.duplication).map(Some);
349 }
350 super::duplication::run_duplication_with_session(
351 &ctx.prepared.duplication,
352 ctx.resolved,
353 ctx.session,
354 ctx.changed_files,
355 Instant::now(),
356 )
357 .map(Some)
358}
359
360fn run_combined_health(
361 ctx: &DeadCodeSessionRun<'_>,
362 share_health: bool,
363 dead_code_artifacts: Option<DeadCodeAnalysisArtifacts>,
364 pre_computed_duplication: Option<fallow_engine::duplicates::DuplicationReport>,
365) -> ProgrammaticResult<Option<crate::HealthProgrammaticOutput>> {
366 if !ctx.options.health {
367 return Ok(None);
368 }
369 if !share_health {
370 return run_health(&ctx.prepared.health).map(Some);
371 }
372 run_health_with_session_artifacts(
373 &ctx.prepared.health,
374 ctx.resolved,
375 ctx.session,
376 ctx.changed_files,
377 dead_code_artifacts,
378 pre_computed_duplication,
379 )
380 .map(Some)
381}
382
383fn run_combined_sections_isolated(
384 options: &CombinedOptions,
385 resolved: &crate::analysis_context::ProgrammaticAnalysisContext,
386 prepared: &PreparedCombinedOptions,
387) -> ProgrammaticResult<CombinedSectionRun> {
388 Ok(CombinedSectionRun {
389 dead_code: options
390 .dead_code
391 .then(|| super::dead_code::run_dead_code(&prepared.dead_code))
392 .transpose()?,
393 duplication: options
394 .duplication
395 .then(|| run_duplication(&prepared.duplication))
396 .transpose()?,
397 health: options
398 .health
399 .then(|| run_health(&prepared.health))
400 .transpose()?,
401 root: resolved.root().to_path_buf(),
402 workspaces: None,
403 })
404}
405
406fn combined_dead_code_options(options: &CombinedOptions, production: bool) -> DeadCodeOptions {
407 DeadCodeOptions {
408 analysis: analysis_with_effective_production(&options.analysis, production),
409 filters: DeadCodeFilters::default(),
410 files: Vec::new(),
411 include_entry_exports: options.include_entry_exports,
412 }
413}
414
415fn combined_duplication_options(options: &CombinedOptions, production: bool) -> DuplicationOptions {
416 let mut duplication = options.duplication_options.clone();
417 duplication.analysis = analysis_with_effective_production(&options.analysis, production);
418 duplication
419}
420
421fn duplication_options_preserve_health_config(options: &DuplicationOptions) -> bool {
422 options.mode.is_none()
423 && options.min_tokens.is_none()
424 && options.min_lines.is_none()
425 && options.min_occurrences.is_none()
426 && options.threshold.is_none()
427 && options.skip_local.is_none()
428 && options.cross_language.is_none()
429 && options.ignore_imports.is_none()
430}
431
432fn combined_health_options(options: &CombinedOptions, production: bool) -> ComplexityOptions {
433 let mut health = options.health_options.clone();
434 health.analysis = analysis_with_effective_production(&options.analysis, production);
435 health
436}
437
438fn analysis_with_effective_production(
439 analysis: &AnalysisOptions,
440 production: bool,
441) -> AnalysisOptions {
442 AnalysisOptions {
443 production,
444 production_override: Some(production),
445 ..analysis.clone()
446 }
447}
448
449fn combined_next_steps(
450 dead_code: Option<&crate::DeadCodeProgrammaticOutput>,
451 duplication: Option<&crate::DuplicationProgrammaticOutput>,
452 health: Option<&crate::HealthProgrammaticOutput>,
453 root: &std::path::Path,
454 workspaces: Option<&[WorkspaceInfo]>,
455) -> Vec<fallow_types::output::NextStep> {
456 let clone_fingerprints = duplication
457 .map(|duplication| {
458 duplication
459 .output
460 .report
461 .clone_groups
462 .iter()
463 .map(|group| group.fingerprint.as_str())
464 .collect::<Vec<_>>()
465 })
466 .unwrap_or_default();
467 let audit_changed = fallow_engine::churn::is_git_repo(root);
468 let workspace_ref = audit_changed
469 .then(|| {
470 workspaces.map_or_else(
471 || default_workspace_ref(root),
472 |workspaces| default_workspace_ref_for_workspaces(root, workspaces),
473 )
474 })
475 .flatten();
476 build_combined_next_steps(&CombinedNextStepsInput {
477 suggestions_enabled: suggestions_enabled(),
478 has_dead_code_findings: dead_code
479 .is_some_and(|dead_code| dead_code.output.results.total_issues() > 0),
480 trace_unused_export: dead_code.and_then(|dead_code| {
481 fallow_output::trace_unused_export_input(&dead_code.output.results, root)
482 }),
483 workspace_ref: workspace_ref.as_deref(),
484 clone_fingerprints: &clone_fingerprints,
485 has_complexity_findings: health.is_some_and(|health| !health.report.findings.is_empty()),
486 offer_setup: setup_pointer_applicable(root),
487 impact_digest: None,
488 audit_changed,
489 })
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use crate::DuplicationMode;
496
497 #[test]
498 fn health_reuses_combined_duplication_only_without_detector_overrides() {
499 assert!(duplication_options_preserve_health_config(
500 &DuplicationOptions::default()
501 ));
502 assert!(!duplication_options_preserve_health_config(
503 &DuplicationOptions {
504 min_tokens: Some(1),
505 ..DuplicationOptions::default()
506 }
507 ));
508 assert!(!duplication_options_preserve_health_config(
509 &DuplicationOptions {
510 mode: Some(DuplicationMode::Semantic),
511 ..DuplicationOptions::default()
512 }
513 ));
514 }
515}