1use std::path::PathBuf;
4
5use fallow_config::{FallowConfig, 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 trace;
21
22pub use crate::runtime_output::{
23 AuditProgrammaticKeySnapshot, AuditProgrammaticOutput, BoundaryViolationsOutput,
24 BoundaryViolationsProgrammaticOutput, CircularDependenciesOutput,
25 CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput, DeadCodeOutput,
26 DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput, DuplicationOutput,
27 DuplicationProgrammaticOutput, FeatureFlagsOutput, FeatureFlagsProgrammaticOutput,
28 HealthJsonReportInput, HealthProgrammaticOutput, TraceClassMemberOutput, TraceCloneOutput,
29 TraceCloneProgrammaticOutput, TraceDependencyOutput, TraceDependencyProgrammaticOutput,
30 TraceExportOutput, TraceExportProgrammaticOutput, TraceExportTargetOutput, TraceFileOutput,
31 TraceFileProgrammaticOutput, serialize_health_report_json,
32};
33pub use audit::run_audit;
34pub use combined::run_combined;
35pub use dead_code::{run_boundary_violations, run_circular_dependencies, run_dead_code};
36pub use decision_surface::run_decision_surface;
37pub use duplication::run_duplication;
38pub use feature_flags::run_feature_flags;
39pub use trace::{run_trace_clone, run_trace_dependency, run_trace_export, run_trace_file};
40
41use crate::{
42 ComplexityOptions, ProgrammaticError,
43 analysis_context::{
44 ProgrammaticAnalysisContext, resolve_programmatic_analysis_context,
45 workspace_roots_for_session,
46 },
47 derive_complexity_options,
48 next_steps::{setup_pointer_applicable, suggestions_enabled},
49};
50
51type ProgrammaticResult<T> = Result<T, ProgrammaticError>;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub(super) struct EffectiveProductionModes {
55 pub dead_code: bool,
56 pub health: bool,
57 pub dupes: bool,
58}
59
60pub(super) fn resolve_effective_production_modes(
61 resolved: &ProgrammaticAnalysisContext,
62 dead_code_override: Option<bool>,
63 health_override: Option<bool>,
64 dupes_override: Option<bool>,
65) -> ProgrammaticResult<EffectiveProductionModes> {
66 let config = load_context_production_config(resolved)?;
67 Ok(EffectiveProductionModes {
68 dead_code: effective_production_mode(
69 config,
70 ProductionAnalysis::DeadCode,
71 resolved,
72 dead_code_override,
73 ),
74 health: effective_production_mode(
75 config,
76 ProductionAnalysis::Health,
77 resolved,
78 health_override,
79 ),
80 dupes: effective_production_mode(
81 config,
82 ProductionAnalysis::Dupes,
83 resolved,
84 dupes_override,
85 ),
86 })
87}
88
89fn effective_production_mode(
90 config: ProductionConfig,
91 analysis: ProductionAnalysis,
92 resolved: &ProgrammaticAnalysisContext,
93 analysis_override: Option<bool>,
94) -> bool {
95 analysis_override
96 .or_else(|| resolved.production_override())
97 .unwrap_or_else(|| config.for_analysis(analysis))
98}
99
100fn load_context_production_config(
101 resolved: &ProgrammaticAnalysisContext,
102) -> ProgrammaticResult<ProductionConfig> {
103 let load_options = fallow_config::ConfigLoadOptions {
104 allow_remote_extends: resolved.allow_remote_extends(),
105 };
106 let loaded = if let Some(path) = resolved.config_path().as_deref() {
107 FallowConfig::load_with_options(path, load_options)
108 .map(Some)
109 .map_err(|err| {
110 ProgrammaticError::new(format!("failed to load config: {err:#}"), 2)
111 .with_code("FALLOW_CONFIG_LOAD_FAILED")
112 .with_context("analysis.configPath")
113 })?
114 } else {
115 FallowConfig::find_and_load_with_options(resolved.root(), load_options)
116 .map(|found| found.map(|(config, _)| config))
117 .map_err(|err| {
118 ProgrammaticError::new(format!("failed to load config: {err}"), 2)
119 .with_code("FALLOW_CONFIG_LOAD_FAILED")
120 .with_context("analysis.configPath")
121 })?
122 };
123 Ok(loaded.map_or_else(ProductionConfig::default, |config| config.production))
124}
125
126pub(super) fn health_may_consume_dead_code_artifacts(
127 options: &ComplexityOptions,
128 config: &fallow_config::ResolvedConfig,
129) -> bool {
130 let sections = derive_complexity_options(options);
131 let max_crap = options.max_crap.unwrap_or(config.health.max_crap);
132 sections.file_scores
133 || sections.coverage_gaps
134 || sections.hotspots
135 || sections.targets
136 || sections.force_full
137 || max_crap > 0.0
138}
139
140pub(super) fn health_may_consume_duplication_report(options: &ComplexityOptions) -> bool {
141 let sections = derive_complexity_options(options);
142 sections.score || sections.targets
143}
144
145pub struct ProgrammaticHealthNextStepFacts {
150 pub suggestions_enabled: bool,
152 pub offer_setup: bool,
154 pub impact_digest: Option<fallow_output::ImpactDigestCounts>,
156 pub audit_changed: bool,
158}
159
160pub struct ProgrammaticHealthAnalysis {
165 pub report: HealthReport,
167 pub grouping: Option<HealthGrouping>,
169 pub root: PathBuf,
171 pub elapsed: std::time::Duration,
173}
174
175impl ProgrammaticHealthAnalysis {
176 fn from_engine<GroupResolver>(
177 analysis: fallow_engine::health::HealthAnalysisResult<GroupResolver>,
178 ) -> Self {
179 Self {
180 root: analysis.config.root,
181 report: analysis.report,
182 grouping: analysis.grouping,
183 elapsed: analysis.elapsed,
184 }
185 }
186}
187
188pub struct ProgrammaticHealthRun {
193 pub analysis: ProgrammaticHealthAnalysis,
195 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
197 pub next_step_facts: ProgrammaticHealthNextStepFacts,
199 pub telemetry_analysis_run_id: Option<String>,
201}
202
203pub trait ProgrammaticHealthRunner {
208 fn run_programmatic_health(
215 &self,
216 options: &ComplexityOptions,
217 ) -> Result<ProgrammaticHealthRun, ProgrammaticError>;
218}
219
220#[derive(Debug, Clone, Copy, Default)]
229pub struct EngineHealthRunner;
230
231impl ProgrammaticHealthRunner for EngineHealthRunner {
232 fn run_programmatic_health(
233 &self,
234 options: &ComplexityOptions,
235 ) -> Result<ProgrammaticHealthRun, ProgrammaticError> {
236 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
237 resolved.install(|| run_programmatic_health_on_engine(&resolved, options))
238 }
239}
240
241fn run_programmatic_health_on_engine(
242 resolved: &ProgrammaticAnalysisContext,
243 options: &ComplexityOptions,
244) -> ProgrammaticResult<ProgrammaticHealthRun> {
245 let health_options = derive_programmatic_health_execution_options(resolved, options);
246 let result = fallow_engine::health::run_ungrouped_health(
247 &health_options,
248 resolved.workspace_roots.clone(),
249 )
250 .map_err(|_| generic_health_error("health"))?;
251
252 Ok(programmatic_health_run_from_engine_result(result))
253}
254
255fn programmatic_health_run_from_engine_result<GroupResolver>(
256 result: fallow_engine::health::HealthAnalysisResult<GroupResolver>,
257) -> ProgrammaticHealthRun {
258 let root = result.config.root.clone();
259 let next_step_facts = ProgrammaticHealthNextStepFacts {
260 suggestions_enabled: suggestions_enabled(),
261 offer_setup: setup_pointer_applicable(&root),
262 impact_digest: None,
263 audit_changed: fallow_engine::churn::is_git_repo(&root),
264 };
265 ProgrammaticHealthRun {
266 workspace_diagnostics: result.workspace_diagnostics.clone(),
267 analysis: ProgrammaticHealthAnalysis::from_engine(result.without_group_resolver()),
268 next_step_facts,
269 telemetry_analysis_run_id: None,
270 }
271}
272
273#[cfg(test)]
274pub(super) fn run_health_with_session(
275 options: &ComplexityOptions,
276 resolved: &ProgrammaticAnalysisContext,
277 session: &AnalysisSession,
278 changed_files: Option<&FxHashSet<PathBuf>>,
279) -> ProgrammaticResult<HealthProgrammaticOutput> {
280 run_health_with_session_artifacts(options, resolved, session, changed_files, None, None)
281}
282
283pub(super) fn run_health_with_session_artifacts(
284 options: &ComplexityOptions,
285 resolved: &ProgrammaticAnalysisContext,
286 session: &AnalysisSession,
287 changed_files: Option<&FxHashSet<PathBuf>>,
288 pre_computed_analysis: Option<DeadCodeAnalysisArtifacts>,
289 pre_computed_duplication: Option<DuplicationReport>,
290) -> ProgrammaticResult<HealthProgrammaticOutput> {
291 crate::validate_complexity_options(options)?;
292 let health_options = derive_programmatic_health_execution_options(resolved, options);
293 let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
294 let result = fallow_engine::health::run_ungrouped_health_with_session_artifacts(
295 &health_options,
296 workspace_roots,
297 session,
298 changed_files.map(|files| files.iter().cloned().collect()),
299 pre_computed_analysis,
300 pre_computed_duplication,
301 )
302 .map_err(|_| generic_health_error("health"))?;
303
304 Ok(assemble_health_programmatic_output(
305 options,
306 programmatic_health_run_from_engine_result(result),
307 ))
308}
309
310fn generic_health_error(command: &str) -> ProgrammaticError {
311 let code = format!(
312 "FALLOW_{}_FAILED",
313 command.replace('-', "_").to_ascii_uppercase()
314 );
315 ProgrammaticError::new(format!("{command} failed"), 2)
316 .with_code(code)
317 .with_context(format!("fallow {command}"))
318 .with_help(format!(
319 "Re-run `fallow {command} --format json --quiet` in the target project for CLI diagnostics"
320 ))
321}
322
323pub fn run_health(options: &ComplexityOptions) -> ProgrammaticResult<HealthProgrammaticOutput> {
330 run_health_with_runner(options, &EngineHealthRunner)
331}
332
333#[must_use]
334fn derive_programmatic_health_execution_options<'a>(
335 resolved: &'a ProgrammaticAnalysisContext,
336 options: &'a ComplexityOptions,
337) -> fallow_engine::health::HealthExecutionOptions<'a> {
338 let run = crate::derive_complexity_run_options(options);
339
340 fallow_engine::health::HealthExecutionOptions {
341 root: resolved.root(),
342 config_path: resolved.config_path(),
343 output: OutputFormat::Human,
344 no_cache: resolved.no_cache(),
345 threads: resolved.threads(),
346 quiet: true,
347 complexity_breakdown: run.complexity_breakdown,
348 thresholds: crate::thresholds_to_engine(run.thresholds),
349 top: run.top,
350 sort: crate::complexity_sort_to_engine(run.sort),
351 production: resolved.production_override().unwrap_or(false),
352 production_override: resolved.production_override(),
353 allow_remote_extends: resolved.allow_remote_extends(),
354 changed_since: resolved.changed_since(),
355 diff_index: resolved.diff_index(),
356 use_shared_diff_index: false,
357 workspace: resolved.workspace(),
358 changed_workspaces: resolved.changed_workspaces(),
359 baseline: None,
360 save_baseline: None,
361 baseline_mode: fallow_engine::baseline::HealthBaselineMode::Count,
362 baseline_mode_explicit: false,
363 complexity: run.sections.complexity,
364 file_scores: run.sections.file_scores,
365 coverage_gaps: run.sections.coverage_gaps,
366 config_activates_coverage_gaps: !run.sections.any_section,
367 hotspots: run.sections.hotspots,
368 ownership: run.sections.ownership,
369 targets: run.sections.targets,
370 css: run.css,
371 css_deep: run.css_deep,
372 force_full: run.sections.force_full,
373 score_only_output: run.sections.score_only_output,
374 enforce_coverage_gap_gate: true,
375 effort: run.effort.map(crate::target_effort_to_output),
376 score: run.sections.score,
377 gates: fallow_engine::health::HealthGateOptions::default(),
378 since: run.since,
379 min_commits: run.min_commits,
380 explain: resolved.explain_enabled(),
381 summary: false,
382 save_snapshot: None,
383 trend: false,
384 coverage_inputs: crate::coverage_inputs_to_engine(run.coverage_inputs),
385 performance: false,
386 runtime_coverage: None,
387 churn_file: None,
388 analysis_identity: fallow_types::semantic::SemanticAnalysisIdentity::default(),
389 group_by: None,
390 ownership_emails: run
391 .ownership_emails
392 .map(crate::ownership_email_mode_to_config),
393 }
394}
395
396pub fn run_complexity_with_runner(
408 options: &ComplexityOptions,
409 runner: &impl ProgrammaticHealthRunner,
410) -> ProgrammaticResult<HealthProgrammaticOutput> {
411 crate::validate_complexity_options(options)?;
412 Ok(assemble_health_programmatic_output(
413 options,
414 runner.run_programmatic_health(options)?,
415 ))
416}
417
418fn assemble_health_programmatic_output(
419 options: &ComplexityOptions,
420 run: ProgrammaticHealthRun,
421) -> HealthProgrammaticOutput {
422 let ProgrammaticHealthRun {
423 analysis,
424 workspace_diagnostics,
425 next_step_facts,
426 telemetry_analysis_run_id,
427 } = run;
428 let root = analysis.root.clone();
429 let next_steps =
430 fallow_output::build_health_next_steps(fallow_output::build_health_next_steps_input(
431 &analysis.report,
432 next_step_facts.suggestions_enabled,
433 next_step_facts.offer_setup,
434 next_step_facts.impact_digest,
435 next_step_facts.audit_changed,
436 ));
437 HealthProgrammaticOutput {
438 report: analysis.report,
439 grouping: analysis.grouping,
440 root,
441 elapsed: analysis.elapsed,
442 explain: options.analysis.explain,
443 workspace_diagnostics,
444 next_steps,
445 envelope_mode: root_envelope_mode(),
446 telemetry_analysis_run_id,
447 }
448}
449
450pub fn run_health_with_runner(
456 options: &ComplexityOptions,
457 runner: &impl ProgrammaticHealthRunner,
458) -> ProgrammaticResult<HealthProgrammaticOutput> {
459 run_complexity_with_runner(options, runner)
460}
461
462const fn root_envelope_mode() -> RootEnvelopeMode {
463 RootEnvelopeMode::Tagged
464}
465
466#[cfg(test)]
467mod tests;