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