1use std::path::{Path, PathBuf};
4
5use fallow_config::{FallowConfig, HealthConfig, ProductionAnalysis, ProductionConfig};
6use fallow_engine::{
7 dead_code::DeadCodeAnalysisArtifacts, duplicates::DuplicationReport, session::AnalysisSession,
8};
9use fallow_output::{HealthGrouping, HealthReport, RootEnvelopeMode};
10use fallow_types::output_format::OutputFormat;
11use fallow_types::workspace::WorkspaceDiagnostic;
12use rustc_hash::FxHashSet;
13
14mod audit;
15mod combined;
16mod dead_code;
17mod decision_surface;
18mod duplication;
19mod feature_flags;
20mod similar_code;
21mod trace;
22
23pub use crate::runtime_output::{
24 AuditProgrammaticKeySnapshot, AuditProgrammaticOutput, BoundaryViolationsOutput,
25 BoundaryViolationsProgrammaticOutput, CircularDependenciesOutput,
26 CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput, DeadCodeOutput,
27 DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput, DuplicationOutput,
28 DuplicationProgrammaticOutput, FeatureFlagsOutput, FeatureFlagsProgrammaticOutput,
29 HealthJsonReportInput, HealthProgrammaticOutput, TraceClassMemberOutput, TraceCloneOutput,
30 TraceCloneProgrammaticOutput, TraceDependencyOutput, TraceDependencyProgrammaticOutput,
31 TraceErrorOutput, TraceErrorProgrammaticOutput, TraceExportOutput,
32 TraceExportProgrammaticOutput, TraceExportTargetOutput, TraceFileOutput,
33 TraceFileProgrammaticOutput, TraceImportPathOutput, TraceImportPathProgrammaticOutput,
34 serialize_health_report_json,
35};
36pub use audit::run_audit;
37pub use combined::run_combined;
38pub use dead_code::{run_boundary_violations, run_circular_dependencies, run_dead_code};
39pub use decision_surface::run_decision_surface;
40pub use duplication::run_duplication;
41pub use feature_flags::run_feature_flags;
42pub use similar_code::{
43 inspect_similar_code, parse_similar_code_candidate_snapshot, review_similar_code,
44 run_similar_code, select_similar_code_candidate_snapshot,
45};
46pub use trace::{
47 TraceCloneBenchmarkResult, benchmark_trace_clone_compact_json,
48 benchmark_trace_graph_family_compact_json, run_trace_clone, run_trace_dependency,
49 run_trace_error, run_trace_export, run_trace_file, run_trace_import_path,
50};
51
52use crate::{
53 ComplexityOptions, ProgrammaticError,
54 analysis_context::{
55 ProgrammaticAnalysisContext, resolve_programmatic_analysis_context,
56 workspace_roots_for_session,
57 },
58 derive_complexity_options,
59 next_steps::{setup_pointer_applicable, suggestions_enabled},
60};
61
62type ProgrammaticResult<T> = Result<T, ProgrammaticError>;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub(super) struct EffectiveProductionModes {
66 pub dead_code: bool,
67 pub health: bool,
68 pub dupes: bool,
69}
70
71pub(super) fn resolve_effective_production_modes(
72 resolved: &ProgrammaticAnalysisContext,
73 dead_code_override: Option<bool>,
74 health_override: Option<bool>,
75 dupes_override: Option<bool>,
76) -> ProgrammaticResult<EffectiveProductionModes> {
77 let config = load_context_production_config(resolved)?;
78 Ok(EffectiveProductionModes {
79 dead_code: effective_production_mode(
80 config,
81 ProductionAnalysis::DeadCode,
82 resolved,
83 dead_code_override,
84 ),
85 health: effective_production_mode(
86 config,
87 ProductionAnalysis::Health,
88 resolved,
89 health_override,
90 ),
91 dupes: effective_production_mode(
92 config,
93 ProductionAnalysis::Dupes,
94 resolved,
95 dupes_override,
96 ),
97 })
98}
99
100fn effective_production_mode(
101 config: ProductionConfig,
102 analysis: ProductionAnalysis,
103 resolved: &ProgrammaticAnalysisContext,
104 analysis_override: Option<bool>,
105) -> bool {
106 analysis_override
107 .or_else(|| resolved.production_override())
108 .unwrap_or_else(|| config.for_analysis(analysis))
109}
110
111fn load_context_production_config(
112 resolved: &ProgrammaticAnalysisContext,
113) -> ProgrammaticResult<ProductionConfig> {
114 let loaded = load_config_file(
115 resolved.root(),
116 resolved.config_path().as_deref(),
117 resolved.allow_remote_extends(),
118 )?;
119 Ok(loaded.map_or_else(ProductionConfig::default, |config| config.production))
120}
121
122pub fn load_health_config(
133 options: &crate::AnalysisOptions,
134) -> ProgrammaticResult<Option<HealthConfig>> {
135 let root = crate::analysis_context::resolve_analysis_root(options.root.as_deref())?;
136 crate::analysis_context::validate_analysis_config_path(options.config_path.as_deref())?;
137 let loaded = load_config_file(
138 &root,
139 options.config_path.as_deref(),
140 options.allow_remote_extends,
141 )?;
142 Ok(loaded.map(|config| config.health))
143}
144
145fn load_config_file(
146 root: &Path,
147 config_path: Option<&Path>,
148 allow_remote_extends: bool,
149) -> ProgrammaticResult<Option<FallowConfig>> {
150 let load_options = fallow_config::ConfigLoadOptions {
151 allow_remote_extends,
152 };
153 if let Some(path) = config_path {
154 return FallowConfig::load_with_options(path, load_options)
155 .map(Some)
156 .map_err(|err| config_load_error(format!("failed to load config: {err:#}")));
157 }
158 FallowConfig::find_and_load_with_options(root, load_options)
159 .map(|found| found.map(|(config, _)| config))
160 .map_err(|err| config_load_error(format!("failed to load config: {err}")))
161}
162
163fn config_load_error(message: String) -> ProgrammaticError {
164 ProgrammaticError::new(message, 2)
165 .with_code("FALLOW_CONFIG_LOAD_FAILED")
166 .with_context("analysis.configPath")
167}
168
169pub(super) fn health_may_consume_dead_code_artifacts(
170 options: &ComplexityOptions,
171 config: &fallow_config::ResolvedConfig,
172) -> bool {
173 let sections = derive_complexity_options(options);
174 let max_crap = options.max_crap.unwrap_or(config.health.max_crap);
175 sections.file_scores
176 || sections.coverage_gaps
177 || sections.hotspots
178 || sections.targets
179 || sections.force_full
180 || max_crap > 0.0
181}
182
183pub(super) fn health_may_consume_duplication_report(options: &ComplexityOptions) -> bool {
184 let sections = derive_complexity_options(options);
185 sections.score || sections.targets
186}
187
188pub struct ProgrammaticHealthNextStepFacts {
193 pub suggestions_enabled: bool,
195 pub offer_setup: bool,
197 pub impact_digest: Option<fallow_output::ImpactDigestCounts>,
199 pub audit_changed: bool,
201}
202
203pub struct ProgrammaticHealthAnalysis {
208 pub report: HealthReport,
210 pub grouping: Option<HealthGrouping>,
212 pub root: PathBuf,
214 pub elapsed: std::time::Duration,
216}
217
218impl ProgrammaticHealthAnalysis {
219 fn from_engine<GroupResolver>(
220 analysis: fallow_engine::health::HealthAnalysisResult<GroupResolver>,
221 ) -> Self {
222 Self {
223 root: analysis.config.root,
224 report: analysis.report,
225 grouping: analysis.grouping,
226 elapsed: analysis.elapsed,
227 }
228 }
229}
230
231pub struct ProgrammaticHealthRun {
236 pub analysis: ProgrammaticHealthAnalysis,
238 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
240 pub next_step_facts: ProgrammaticHealthNextStepFacts,
242 pub telemetry_analysis_run_id: Option<String>,
244}
245
246pub trait ProgrammaticHealthRunner {
251 fn run_programmatic_health(
258 &self,
259 options: &ComplexityOptions,
260 ) -> Result<ProgrammaticHealthRun, ProgrammaticError>;
261}
262
263#[derive(Debug, Clone, Copy, Default)]
272pub struct EngineHealthRunner;
273
274impl ProgrammaticHealthRunner for EngineHealthRunner {
275 fn run_programmatic_health(
276 &self,
277 options: &ComplexityOptions,
278 ) -> Result<ProgrammaticHealthRun, ProgrammaticError> {
279 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
280 resolved.install(|| run_programmatic_health_on_engine(&resolved, options))
281 }
282}
283
284fn run_programmatic_health_on_engine(
285 resolved: &ProgrammaticAnalysisContext,
286 options: &ComplexityOptions,
287) -> ProgrammaticResult<ProgrammaticHealthRun> {
288 let health_options = derive_programmatic_health_execution_options(resolved, options);
289 let result = fallow_engine::health::run_ungrouped_health(
290 &health_options,
291 resolved.workspace_roots.clone(),
292 )
293 .map_err(|error| programmatic_health_error("health", error))?;
294
295 Ok(programmatic_health_run_from_engine_result(result))
296}
297
298fn programmatic_health_run_from_engine_result<GroupResolver>(
299 result: fallow_engine::health::HealthAnalysisResult<GroupResolver>,
300) -> ProgrammaticHealthRun {
301 let root = result.config.root.clone();
302 let next_step_facts = ProgrammaticHealthNextStepFacts {
303 suggestions_enabled: suggestions_enabled(),
304 offer_setup: setup_pointer_applicable(&root),
305 impact_digest: None,
306 audit_changed: fallow_engine::churn::is_git_repo(&root),
307 };
308 ProgrammaticHealthRun {
309 workspace_diagnostics: result.workspace_diagnostics.clone(),
310 analysis: ProgrammaticHealthAnalysis::from_engine(result.without_group_resolver()),
311 next_step_facts,
312 telemetry_analysis_run_id: None,
313 }
314}
315
316#[cfg(test)]
317pub(super) fn run_health_with_session(
318 options: &ComplexityOptions,
319 resolved: &ProgrammaticAnalysisContext,
320 session: &AnalysisSession,
321 changed_files: Option<&FxHashSet<PathBuf>>,
322) -> ProgrammaticResult<HealthProgrammaticOutput> {
323 run_health_with_session_artifacts(options, resolved, session, changed_files, None, None)
324}
325
326pub(super) fn run_health_with_session_artifacts(
327 options: &ComplexityOptions,
328 resolved: &ProgrammaticAnalysisContext,
329 session: &AnalysisSession,
330 changed_files: Option<&FxHashSet<PathBuf>>,
331 pre_computed_analysis: Option<DeadCodeAnalysisArtifacts>,
332 pre_computed_duplication: Option<DuplicationReport>,
333) -> ProgrammaticResult<HealthProgrammaticOutput> {
334 crate::validate_complexity_options(options)?;
335 let health_options = derive_programmatic_health_execution_options(resolved, options);
336 let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
337 let result = fallow_engine::health::run_ungrouped_health_with_session_artifacts(
338 &health_options,
339 workspace_roots,
340 session,
341 changed_files.map(|files| files.iter().cloned().collect()),
342 pre_computed_analysis,
343 pre_computed_duplication,
344 )
345 .map_err(|error| programmatic_health_error("health", error))?;
346
347 Ok(assemble_health_programmatic_output(
348 options,
349 programmatic_health_run_from_engine_result(result),
350 ))
351}
352
353fn programmatic_health_error(
354 command: &str,
355 error: fallow_engine::health::HealthError,
356) -> ProgrammaticError {
357 let (message, exit_code) = match error {
358 fallow_engine::health::HealthError::Message { message, exit_code } => (message, exit_code),
359 fallow_engine::health::HealthError::Printed(exit_code) => {
360 (format!("{command} failed"), exit_code)
361 }
362 };
363 let code = format!(
364 "FALLOW_{}_FAILED",
365 command.replace('-', "_").to_ascii_uppercase()
366 );
367 ProgrammaticError::new(message, exit_code)
368 .with_code(code)
369 .with_context(format!("fallow {command}"))
370 .with_help(format!(
371 "Re-run `fallow {command} --format json --quiet` in the target project for CLI diagnostics"
372 ))
373}
374
375pub fn run_health(options: &ComplexityOptions) -> ProgrammaticResult<HealthProgrammaticOutput> {
382 run_health_with_runner(options, &EngineHealthRunner)
383}
384
385#[must_use]
386fn derive_programmatic_health_execution_options<'a>(
387 resolved: &'a ProgrammaticAnalysisContext,
388 options: &'a ComplexityOptions,
389) -> fallow_engine::health::HealthExecutionOptions<'a> {
390 let run = crate::derive_complexity_run_options(options);
391
392 fallow_engine::health::HealthExecutionOptions {
393 root: resolved.root(),
394 config_path: resolved.config_path(),
395 output: OutputFormat::Human,
396 no_cache: resolved.no_cache(),
397 threads: resolved.threads(),
398 quiet: true,
399 complexity_breakdown: run.complexity_breakdown,
400 thresholds: crate::thresholds_to_engine(run.thresholds),
401 top: run.top,
402 sort: crate::complexity_sort_to_engine(run.sort),
403 production: resolved.production_override().unwrap_or(false),
404 production_override: resolved.production_override(),
405 allow_remote_extends: resolved.allow_remote_extends(),
406 changed_since: resolved.changed_since(),
407 diff_index: resolved.diff_index(),
408 use_shared_diff_index: false,
409 workspace: resolved.workspace(),
410 changed_workspaces: resolved.changed_workspaces(),
411 baseline: None,
412 save_baseline: None,
413 baseline_mode: fallow_engine::baseline::HealthBaselineMode::Count,
414 baseline_mode_explicit: false,
415 complexity: run.sections.complexity,
416 file_scores: run.sections.file_scores,
417 coverage_gaps: run.sections.coverage_gaps,
418 config_activates_coverage_gaps: !run.sections.any_section,
419 hotspots: run.sections.hotspots,
420 ownership: run.sections.ownership,
421 targets: run.sections.targets,
422 css: run.css,
423 css_deep: run.css_deep,
424 force_full: run.sections.force_full,
425 score_only_output: run.sections.score_only_output,
426 enforce_coverage_gap_gate: true,
427 effort: run.effort.map(crate::target_effort_to_output),
428 score: run.sections.score,
429 gates: fallow_engine::health::HealthGateOptions::default(),
430 since: run.since,
431 min_commits: run.min_commits,
432 explain: resolved.explain_enabled(),
433 summary: false,
434 save_snapshot: None,
435 trend: false,
436 coverage_inputs: crate::coverage_inputs_to_engine(run.coverage_inputs),
437 performance: false,
438 runtime_coverage: None,
439 churn_file: None,
440 analysis_identity: fallow_types::semantic::SemanticAnalysisIdentity::default(),
441 group_by: None,
442 ownership_emails: run
443 .ownership_emails
444 .map(crate::ownership_email_mode_to_config),
445 }
446}
447
448pub fn run_complexity_with_runner(
460 options: &ComplexityOptions,
461 runner: &impl ProgrammaticHealthRunner,
462) -> ProgrammaticResult<HealthProgrammaticOutput> {
463 crate::validate_complexity_options(options)?;
464 crate::analysis_context::ensure_options_not_cancelled(&options.analysis, "health analysis")?;
465 Ok(assemble_health_programmatic_output(
466 options,
467 runner.run_programmatic_health(options)?,
468 ))
469}
470
471fn assemble_health_programmatic_output(
472 options: &ComplexityOptions,
473 run: ProgrammaticHealthRun,
474) -> HealthProgrammaticOutput {
475 let ProgrammaticHealthRun {
476 analysis,
477 workspace_diagnostics,
478 next_step_facts,
479 telemetry_analysis_run_id,
480 } = run;
481 let root = analysis.root.clone();
482 let next_steps =
483 fallow_output::build_health_next_steps(fallow_output::build_health_next_steps_input(
484 &analysis.report,
485 next_step_facts.suggestions_enabled,
486 next_step_facts.offer_setup,
487 next_step_facts.impact_digest,
488 next_step_facts.audit_changed,
489 ));
490 HealthProgrammaticOutput {
491 report: analysis.report,
492 grouping: analysis.grouping,
493 root,
494 elapsed: analysis.elapsed,
495 explain: options.analysis.explain,
496 workspace_diagnostics,
497 next_steps,
498 envelope_mode: root_envelope_mode(),
499 telemetry_analysis_run_id,
500 }
501}
502
503pub fn run_health_with_runner(
509 options: &ComplexityOptions,
510 runner: &impl ProgrammaticHealthRunner,
511) -> ProgrammaticResult<HealthProgrammaticOutput> {
512 run_complexity_with_runner(options, runner)
513}
514
515const fn root_envelope_mode() -> RootEnvelopeMode {
516 RootEnvelopeMode::Tagged
517}
518
519#[cfg(test)]
520mod cancellation_tests;
521#[cfg(test)]
522mod tests;