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