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