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::{changed_files_for_run, resolve_programmatic_analysis_context},
16 next_steps::{
17 audit_changed_applicable, default_workspace_ref, default_workspace_ref_for_workspaces,
18 setup_pointer_applicable, suggestions_enabled,
19 },
20};
21
22use super::{
23 ProductionModes, ProgrammaticResult, health_may_consume_dead_code_artifacts,
24 health_may_consume_duplication_report, resolve_effective_production_modes, run_duplication,
25 run_health, run_health_with_session_artifacts,
26};
27
28struct PreparedCombinedOptions {
29 dead_code: DeadCodeOptions,
30 duplication: DuplicationOptions,
31 health: ComplexityOptions,
32}
33
34struct CombinedSectionRun {
35 dead_code: Option<crate::DeadCodeProgrammaticOutput>,
36 duplication: Option<crate::DuplicationProgrammaticOutput>,
37 health: Option<crate::HealthProgrammaticOutput>,
38 root: PathBuf,
39 workspaces: Option<Vec<WorkspaceInfo>>,
40}
41
42struct DeadCodeSessionRun<'a> {
43 options: &'a CombinedOptions,
44 resolved: &'a crate::analysis_context::ProgrammaticAnalysisContext,
45 prepared: &'a PreparedCombinedOptions,
46 changed_files: Option<&'a FxHashSet<PathBuf>>,
47 session: &'a AnalysisSession,
48}
49
50pub fn run_combined(options: &CombinedOptions) -> ProgrammaticResult<CombinedProgrammaticOutput> {
57 if !(options.dead_code || options.duplication || options.health) {
58 return Err(ProgrammaticError::new(
59 "combined analysis requires at least one enabled section",
60 2,
61 )
62 .with_code("FALLOW_COMBINED_EMPTY")
63 .with_context("combined"));
64 }
65
66 let start = Instant::now();
67 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
68 resolved.install(|| {
69 resolved.ensure_not_cancelled("combined analysis")?;
70 let production_modes = resolve_effective_production_modes(&resolved, None, None, None)?;
71 let prepared = prepare_combined_options(options, production_modes);
72 let changed_files = changed_files_for_run(&resolved)?;
73 let sections = run_combined_sections(
74 options,
75 &resolved,
76 &prepared,
77 changed_files.as_ref(),
78 production_modes,
79 )?;
80
81 let next_steps = combined_next_steps(
82 sections.dead_code.as_ref(),
83 sections.duplication.as_ref(),
84 sections.health.as_ref(),
85 §ions.root,
86 sections.workspaces.as_deref(),
87 );
88
89 Ok(CombinedProgrammaticOutput {
90 dead_code: sections.dead_code,
91 duplication: sections.duplication,
92 health: sections.health,
93 root: sections.root,
94 elapsed: start.elapsed(),
95 explain: options.analysis.explain,
96 next_steps,
97 telemetry_analysis_run_id: None,
98 request_outcomes: resolved.request_outcomes(),
99 })
100 })
101}
102
103fn prepare_combined_options(
104 options: &CombinedOptions,
105 production_modes: ProductionModes,
106) -> PreparedCombinedOptions {
107 PreparedCombinedOptions {
108 dead_code: combined_dead_code_options(options, production_modes.dead_code),
109 duplication: combined_duplication_options(options, production_modes.dupes),
110 health: combined_health_options(options, production_modes.health),
111 }
112}
113
114fn run_combined_sections(
115 options: &CombinedOptions,
116 resolved: &crate::analysis_context::ProgrammaticAnalysisContext,
117 prepared: &PreparedCombinedOptions,
118 changed_files: Option<&FxHashSet<PathBuf>>,
119 production_modes: ProductionModes,
120) -> ProgrammaticResult<CombinedSectionRun> {
121 let share_health =
122 options.dead_code && options.health && production_modes.dead_code_matches_health();
123 let share_dupes =
124 options.dead_code && options.duplication && production_modes.dead_code_matches_dupes();
125 if share_health || share_dupes {
126 return run_combined_with_dead_code_session(
127 options,
128 resolved,
129 prepared,
130 changed_files,
131 share_health,
132 share_dupes,
133 );
134 }
135 run_combined_sections_isolated(options, resolved, prepared)
136}
137
138fn run_combined_with_dead_code_session(
139 options: &CombinedOptions,
140 resolved: &crate::analysis_context::ProgrammaticAnalysisContext,
141 prepared: &PreparedCombinedOptions,
142 changed_files: Option<&FxHashSet<PathBuf>>,
143 share_health: bool,
144 share_dupes: bool,
145) -> ProgrammaticResult<CombinedSectionRun> {
146 resolved.ensure_not_cancelled("config load and file discovery")?;
147 let session = super::dead_code::load_dead_code_session(&prepared.dead_code, resolved)?;
148 super::dead_code::resolve_package_map_before_analysis(resolved, &session)?;
149 if share_dupes {
150 return run_combined_with_project_artifacts(CombinedProjectArtifactRun {
151 options,
152 resolved,
153 prepared,
154 changed_files,
155 share_health,
156 session: &session,
157 });
158 }
159 let ctx = DeadCodeSessionRun {
160 options,
161 resolved,
162 prepared,
163 changed_files,
164 session: &session,
165 };
166 let (dead_code, dead_code_artifacts) =
167 run_dead_code_with_optional_artifacts(&ctx, options.health && share_health)?;
168 let duplication = run_combined_duplication(&ctx, share_dupes)?;
169 resolved.ensure_not_cancelled("the health section")?;
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(&run, project.duplication, section_start)?;
217 let super::dead_code::DeadCodeProgrammaticRunWithArtifacts {
218 output: dead_code,
219 artifacts,
220 } = dead_code;
221 let dead_code_artifacts = retain_dead_code_artifacts.then_some(artifacts);
222 resolved.ensure_not_cancelled("the health section")?;
223 let health = run_combined_health(
224 &DeadCodeSessionRun {
225 options,
226 resolved,
227 prepared,
228 changed_files,
229 session,
230 },
231 share_health,
232 dead_code_artifacts,
233 pre_computed_duplication_for_health,
234 )?;
235
236 Ok(CombinedSectionRun {
237 dead_code: Some(dead_code),
238 duplication,
239 health,
240 root: session.root().to_path_buf(),
241 workspaces: Some(session.workspaces().to_vec()),
242 })
243}
244
245fn run_project_artifact_duplication(
246 run: &CombinedProjectArtifactRun<'_>,
247 duplication: fallow_engine::duplicates::DuplicationReport,
248 section_start: Instant,
249) -> ProgrammaticResult<Option<crate::DuplicationProgrammaticOutput>> {
250 let CombinedProjectArtifactRun {
251 options,
252 resolved,
253 prepared,
254 changed_files,
255 session,
256 ..
257 } = *run;
258 options
259 .duplication
260 .then(|| {
261 let change_scope =
262 resolved.change_scope(changed_files, session.config(), session.workspaces())?;
263 super::duplication::run_duplication_report_with_session(
264 &prepared.duplication,
265 resolved,
266 session,
267 duplication,
268 &change_scope,
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 finding_ids: Vec::new(),
414 analysis: analysis_with_effective_production(&options.analysis, production),
415 filters: DeadCodeFilters::default(),
416 files: Vec::new(),
417 include_entry_exports: options.include_entry_exports,
418 show_cascade: options.show_cascade,
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.ignore_symlinks.is_none()
436 && options.cross_language.is_none()
437 && options.ignore_imports.is_none()
438}
439
440fn combined_health_options(options: &CombinedOptions, production: bool) -> ComplexityOptions {
441 let mut health = options.health_options.clone();
442 health.analysis = analysis_with_effective_production(&options.analysis, production);
443 health
444}
445
446fn analysis_with_effective_production(
447 analysis: &AnalysisOptions,
448 production: bool,
449) -> AnalysisOptions {
450 AnalysisOptions {
451 production,
452 production_override: Some(production),
453 ..analysis.clone()
454 }
455}
456
457fn combined_next_steps(
458 dead_code: Option<&crate::DeadCodeProgrammaticOutput>,
459 duplication: Option<&crate::DuplicationProgrammaticOutput>,
460 health: Option<&crate::HealthProgrammaticOutput>,
461 root: &std::path::Path,
462 workspaces: Option<&[WorkspaceInfo]>,
463) -> Vec<fallow_types::output::NextStep> {
464 let clone_fingerprints = duplication
465 .map(|duplication| {
466 duplication
467 .output
468 .report
469 .clone_groups
470 .iter()
471 .map(|group| group.fingerprint.as_str())
472 .collect::<Vec<_>>()
473 })
474 .unwrap_or_default();
475 let audit_changed = audit_changed_applicable(root);
476 let workspace_ref = audit_changed
477 .then(|| {
478 workspaces.map_or_else(
479 || default_workspace_ref(root),
480 |workspaces| default_workspace_ref_for_workspaces(root, workspaces),
481 )
482 })
483 .flatten();
484 build_combined_next_steps(&CombinedNextStepsInput {
485 suggestions_enabled: suggestions_enabled(),
486 has_dead_code_findings: dead_code
487 .is_some_and(|dead_code| dead_code.output.results.total_issues() > 0),
488 trace_unused_export: dead_code.and_then(|dead_code| {
489 fallow_output::trace_unused_export_input(&dead_code.output.results, root)
490 }),
491 workspace_ref: workspace_ref.as_deref(),
492 clone_fingerprints: &clone_fingerprints,
493 has_complexity_findings: health.is_some_and(|health| !health.report.findings.is_empty()),
494 offer_setup: setup_pointer_applicable(root),
495 impact_digest: None,
496 audit_changed,
497 has_external_plugins: !fallow_config::discover_external_plugins(root, &[]).is_empty(),
498 has_unused_files: dead_code
499 .is_some_and(|dead_code| !dead_code.output.results.unused_files.is_empty()),
500 baseline_recheck: None,
504 })
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use crate::DuplicationMode;
511
512 #[test]
513 fn health_reuses_combined_duplication_only_without_detector_overrides() {
514 assert!(duplication_options_preserve_health_config(
515 &DuplicationOptions::default()
516 ));
517 assert!(!duplication_options_preserve_health_config(
518 &DuplicationOptions {
519 min_tokens: Some(1),
520 ..DuplicationOptions::default()
521 }
522 ));
523 assert!(!duplication_options_preserve_health_config(
524 &DuplicationOptions {
525 mode: Some(DuplicationMode::Semantic),
526 ..DuplicationOptions::default()
527 }
528 ));
529 }
530}