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