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, TraceCloneOutput,
29 TraceCloneProgrammaticOutput, TraceDependencyOutput, TraceDependencyProgrammaticOutput,
30 TraceExportOutput, TraceExportProgrammaticOutput, TraceFileOutput, TraceFileProgrammaticOutput,
31 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 loaded = if let Some(path) = resolved.config_path().as_deref() {
104 FallowConfig::load(path).map(Some).map_err(|err| {
105 ProgrammaticError::new(format!("failed to load config: {err:#}"), 2)
106 .with_code("FALLOW_CONFIG_LOAD_FAILED")
107 .with_context("analysis.configPath")
108 })?
109 } else {
110 FallowConfig::find_and_load(resolved.root())
111 .map(|found| found.map(|(config, _)| config))
112 .map_err(|err| {
113 ProgrammaticError::new(format!("failed to load config: {err}"), 2)
114 .with_code("FALLOW_CONFIG_LOAD_FAILED")
115 .with_context("analysis.configPath")
116 })?
117 };
118 Ok(loaded.map_or_else(ProductionConfig::default, |config| config.production))
119}
120
121pub(super) fn health_may_consume_dead_code_artifacts(
122 options: &ComplexityOptions,
123 config: &fallow_config::ResolvedConfig,
124) -> bool {
125 let sections = derive_complexity_options(options);
126 let max_crap = options.max_crap.unwrap_or(config.health.max_crap);
127 sections.file_scores
128 || sections.coverage_gaps
129 || sections.hotspots
130 || sections.targets
131 || sections.force_full
132 || max_crap > 0.0
133}
134
135pub(super) fn health_may_consume_duplication_report(options: &ComplexityOptions) -> bool {
136 let sections = derive_complexity_options(options);
137 sections.score || sections.targets
138}
139
140pub struct ProgrammaticHealthNextStepFacts {
145 pub suggestions_enabled: bool,
146 pub offer_setup: bool,
147 pub impact_digest: Option<fallow_output::ImpactDigestCounts>,
148 pub audit_changed: bool,
149}
150
151pub struct ProgrammaticHealthAnalysis {
156 pub report: HealthReport,
157 pub grouping: Option<HealthGrouping>,
158 pub root: PathBuf,
159 pub elapsed: std::time::Duration,
160}
161
162impl ProgrammaticHealthAnalysis {
163 fn from_engine<GroupResolver>(
164 analysis: fallow_engine::health::HealthAnalysisResult<GroupResolver>,
165 ) -> Self {
166 Self {
167 root: analysis.config.root,
168 report: analysis.report,
169 grouping: analysis.grouping,
170 elapsed: analysis.elapsed,
171 }
172 }
173}
174
175pub struct ProgrammaticHealthRun {
180 pub analysis: ProgrammaticHealthAnalysis,
181 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
182 pub next_step_facts: ProgrammaticHealthNextStepFacts,
183 pub telemetry_analysis_run_id: Option<String>,
184}
185
186pub trait ProgrammaticHealthRunner {
191 fn run_programmatic_health(
198 &self,
199 options: &ComplexityOptions,
200 ) -> Result<ProgrammaticHealthRun, ProgrammaticError>;
201}
202
203#[derive(Debug, Clone, Copy, Default)]
212pub struct EngineHealthRunner;
213
214impl ProgrammaticHealthRunner for EngineHealthRunner {
215 fn run_programmatic_health(
216 &self,
217 options: &ComplexityOptions,
218 ) -> Result<ProgrammaticHealthRun, ProgrammaticError> {
219 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
220 resolved.install(|| run_programmatic_health_on_engine(&resolved, options))
221 }
222}
223
224fn run_programmatic_health_on_engine(
225 resolved: &ProgrammaticAnalysisContext,
226 options: &ComplexityOptions,
227) -> ProgrammaticResult<ProgrammaticHealthRun> {
228 let health_options = derive_programmatic_health_execution_options(resolved, options);
229 let result = fallow_engine::health::run_ungrouped_health(
230 &health_options,
231 resolved.workspace_roots.clone(),
232 )
233 .map_err(|_| generic_health_error("health"))?;
234
235 Ok(programmatic_health_run_from_engine_result(result))
236}
237
238fn programmatic_health_run_from_engine_result<GroupResolver>(
239 result: fallow_engine::health::HealthAnalysisResult<GroupResolver>,
240) -> ProgrammaticHealthRun {
241 let root = result.config.root.clone();
242 let next_step_facts = ProgrammaticHealthNextStepFacts {
243 suggestions_enabled: suggestions_enabled(),
244 offer_setup: setup_pointer_applicable(&root),
245 impact_digest: None,
246 audit_changed: fallow_engine::churn::is_git_repo(&root),
247 };
248 ProgrammaticHealthRun {
249 workspace_diagnostics: result.workspace_diagnostics.clone(),
250 analysis: ProgrammaticHealthAnalysis::from_engine(result.without_group_resolver()),
251 next_step_facts,
252 telemetry_analysis_run_id: None,
253 }
254}
255
256#[cfg(test)]
257pub(super) fn run_health_with_session(
258 options: &ComplexityOptions,
259 resolved: &ProgrammaticAnalysisContext,
260 session: &AnalysisSession,
261 changed_files: Option<&FxHashSet<PathBuf>>,
262) -> ProgrammaticResult<HealthProgrammaticOutput> {
263 run_health_with_session_artifacts(options, resolved, session, changed_files, None, None)
264}
265
266pub(super) fn run_health_with_session_artifacts(
267 options: &ComplexityOptions,
268 resolved: &ProgrammaticAnalysisContext,
269 session: &AnalysisSession,
270 changed_files: Option<&FxHashSet<PathBuf>>,
271 pre_computed_analysis: Option<DeadCodeAnalysisArtifacts>,
272 pre_computed_duplication: Option<DuplicationReport>,
273) -> ProgrammaticResult<HealthProgrammaticOutput> {
274 crate::validate_complexity_options(options)?;
275 let health_options = derive_programmatic_health_execution_options(resolved, options);
276 let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
277 let result = fallow_engine::health::run_ungrouped_health_with_session_artifacts(
278 &health_options,
279 workspace_roots,
280 session,
281 changed_files.map(|files| files.iter().cloned().collect()),
282 pre_computed_analysis,
283 pre_computed_duplication,
284 )
285 .map_err(|_| generic_health_error("health"))?;
286
287 Ok(assemble_health_programmatic_output(
288 options,
289 programmatic_health_run_from_engine_result(result),
290 ))
291}
292
293fn generic_health_error(command: &str) -> ProgrammaticError {
294 let code = format!(
295 "FALLOW_{}_FAILED",
296 command.replace('-', "_").to_ascii_uppercase()
297 );
298 ProgrammaticError::new(format!("{command} failed"), 2)
299 .with_code(code)
300 .with_context(format!("fallow {command}"))
301 .with_help(format!(
302 "Re-run `fallow {command} --format json --quiet` in the target project for CLI diagnostics"
303 ))
304}
305
306pub fn run_health(options: &ComplexityOptions) -> ProgrammaticResult<HealthProgrammaticOutput> {
313 run_health_with_runner(options, &EngineHealthRunner)
314}
315
316#[must_use]
317fn derive_programmatic_health_execution_options<'a>(
318 resolved: &'a ProgrammaticAnalysisContext,
319 options: &'a ComplexityOptions,
320) -> fallow_engine::health::HealthExecutionOptions<'a> {
321 let run = crate::derive_complexity_run_options(options);
322
323 fallow_engine::health::HealthExecutionOptions {
324 root: resolved.root(),
325 config_path: resolved.config_path(),
326 output: OutputFormat::Human,
327 no_cache: resolved.no_cache(),
328 threads: resolved.threads(),
329 quiet: true,
330 complexity_breakdown: run.complexity_breakdown,
331 thresholds: crate::thresholds_to_engine(run.thresholds),
332 top: run.top,
333 sort: crate::complexity_sort_to_engine(run.sort),
334 production: resolved.production_override().unwrap_or(false),
335 production_override: resolved.production_override(),
336 changed_since: resolved.changed_since(),
337 diff_index: resolved.diff_index(),
338 use_shared_diff_index: false,
339 workspace: resolved.workspace(),
340 changed_workspaces: resolved.changed_workspaces(),
341 baseline: None,
342 save_baseline: None,
343 complexity: run.sections.complexity,
344 file_scores: run.sections.file_scores,
345 coverage_gaps: run.sections.coverage_gaps,
346 config_activates_coverage_gaps: !run.sections.any_section,
347 hotspots: run.sections.hotspots,
348 ownership: run.sections.ownership,
349 targets: run.sections.targets,
350 css: run.css,
351 css_deep: run.css_deep,
352 force_full: run.sections.force_full,
353 score_only_output: run.sections.score_only_output,
354 enforce_coverage_gap_gate: true,
355 effort: run.effort.map(crate::target_effort_to_output),
356 score: run.sections.score,
357 gates: fallow_engine::health::HealthGateOptions::default(),
358 since: run.since,
359 min_commits: run.min_commits,
360 explain: resolved.explain_enabled(),
361 summary: false,
362 save_snapshot: None,
363 trend: false,
364 coverage_inputs: crate::coverage_inputs_to_engine(run.coverage_inputs),
365 performance: false,
366 runtime_coverage: None,
367 churn_file: None,
368 group_by: None,
369 ownership_emails: run
370 .ownership_emails
371 .map(crate::ownership_email_mode_to_config),
372 }
373}
374
375pub fn run_complexity_with_runner(
387 options: &ComplexityOptions,
388 runner: &impl ProgrammaticHealthRunner,
389) -> ProgrammaticResult<HealthProgrammaticOutput> {
390 crate::validate_complexity_options(options)?;
391 Ok(assemble_health_programmatic_output(
392 options,
393 runner.run_programmatic_health(options)?,
394 ))
395}
396
397fn assemble_health_programmatic_output(
398 options: &ComplexityOptions,
399 run: ProgrammaticHealthRun,
400) -> HealthProgrammaticOutput {
401 let ProgrammaticHealthRun {
402 analysis,
403 workspace_diagnostics,
404 next_step_facts,
405 telemetry_analysis_run_id,
406 } = run;
407 let root = analysis.root.clone();
408 let next_steps =
409 fallow_output::build_health_next_steps(fallow_output::build_health_next_steps_input(
410 &analysis.report,
411 next_step_facts.suggestions_enabled,
412 next_step_facts.offer_setup,
413 next_step_facts.impact_digest,
414 next_step_facts.audit_changed,
415 ));
416 HealthProgrammaticOutput {
417 report: analysis.report,
418 grouping: analysis.grouping,
419 root,
420 elapsed: analysis.elapsed,
421 explain: options.analysis.explain,
422 workspace_diagnostics,
423 next_steps,
424 envelope_mode: root_envelope_mode(),
425 telemetry_analysis_run_id,
426 }
427}
428
429pub fn run_health_with_runner(
435 options: &ComplexityOptions,
436 runner: &impl ProgrammaticHealthRunner,
437) -> ProgrammaticResult<HealthProgrammaticOutput> {
438 run_complexity_with_runner(options, runner)
439}
440
441const fn root_envelope_mode() -> RootEnvelopeMode {
442 RootEnvelopeMode::Tagged
443}
444
445#[cfg(test)]
446mod tests;