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