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