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