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