use std::path::PathBuf;
use fallow_config::{FallowConfig, ProductionAnalysis, ProductionConfig};
use fallow_engine::{
dead_code::DeadCodeAnalysisArtifacts, duplicates::DuplicationReport, session::AnalysisSession,
};
use fallow_output::{HealthGrouping, HealthReport, RootEnvelopeMode};
use fallow_types::output_format::OutputFormat;
use fallow_types::workspace::WorkspaceDiagnostic;
use rustc_hash::FxHashSet;
mod audit;
mod combined;
mod dead_code;
mod decision_surface;
mod duplication;
mod feature_flags;
mod trace;
pub use crate::runtime_output::{
AuditProgrammaticKeySnapshot, AuditProgrammaticOutput, BoundaryViolationsOutput,
BoundaryViolationsProgrammaticOutput, CircularDependenciesOutput,
CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput, DeadCodeOutput,
DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput, DuplicationOutput,
DuplicationProgrammaticOutput, FeatureFlagsOutput, FeatureFlagsProgrammaticOutput,
HealthJsonReportInput, HealthProgrammaticOutput, TraceCloneOutput,
TraceCloneProgrammaticOutput, TraceDependencyOutput, TraceDependencyProgrammaticOutput,
TraceExportOutput, TraceExportProgrammaticOutput, TraceFileOutput, TraceFileProgrammaticOutput,
serialize_health_report_json,
};
pub use audit::run_audit;
pub use combined::run_combined;
pub use dead_code::{run_boundary_violations, run_circular_dependencies, run_dead_code};
pub use decision_surface::run_decision_surface;
pub use duplication::run_duplication;
pub use feature_flags::run_feature_flags;
pub use trace::{run_trace_clone, run_trace_dependency, run_trace_export, run_trace_file};
use crate::{
ComplexityOptions, ProgrammaticError,
analysis_context::{
ProgrammaticAnalysisContext, resolve_programmatic_analysis_context,
workspace_roots_for_session,
},
derive_complexity_options,
next_steps::{setup_pointer_applicable, suggestions_enabled},
};
type ProgrammaticResult<T> = Result<T, ProgrammaticError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct EffectiveProductionModes {
pub dead_code: bool,
pub health: bool,
pub dupes: bool,
}
pub(super) fn resolve_effective_production_modes(
resolved: &ProgrammaticAnalysisContext,
dead_code_override: Option<bool>,
health_override: Option<bool>,
dupes_override: Option<bool>,
) -> ProgrammaticResult<EffectiveProductionModes> {
let config = load_context_production_config(resolved)?;
Ok(EffectiveProductionModes {
dead_code: effective_production_mode(
config,
ProductionAnalysis::DeadCode,
resolved,
dead_code_override,
),
health: effective_production_mode(
config,
ProductionAnalysis::Health,
resolved,
health_override,
),
dupes: effective_production_mode(
config,
ProductionAnalysis::Dupes,
resolved,
dupes_override,
),
})
}
fn effective_production_mode(
config: ProductionConfig,
analysis: ProductionAnalysis,
resolved: &ProgrammaticAnalysisContext,
analysis_override: Option<bool>,
) -> bool {
analysis_override
.or_else(|| resolved.production_override())
.unwrap_or_else(|| config.for_analysis(analysis))
}
fn load_context_production_config(
resolved: &ProgrammaticAnalysisContext,
) -> ProgrammaticResult<ProductionConfig> {
let loaded = if let Some(path) = resolved.config_path().as_deref() {
FallowConfig::load(path).map(Some).map_err(|err| {
ProgrammaticError::new(format!("failed to load config: {err:#}"), 2)
.with_code("FALLOW_CONFIG_LOAD_FAILED")
.with_context("analysis.configPath")
})?
} else {
FallowConfig::find_and_load(resolved.root())
.map(|found| found.map(|(config, _)| config))
.map_err(|err| {
ProgrammaticError::new(format!("failed to load config: {err}"), 2)
.with_code("FALLOW_CONFIG_LOAD_FAILED")
.with_context("analysis.configPath")
})?
};
Ok(loaded.map_or_else(ProductionConfig::default, |config| config.production))
}
pub(super) fn health_may_consume_dead_code_artifacts(
options: &ComplexityOptions,
config: &fallow_config::ResolvedConfig,
) -> bool {
let sections = derive_complexity_options(options);
let max_crap = options.max_crap.unwrap_or(config.health.max_crap);
sections.file_scores
|| sections.coverage_gaps
|| sections.hotspots
|| sections.targets
|| sections.force_full
|| max_crap > 0.0
}
pub(super) fn health_may_consume_duplication_report(options: &ComplexityOptions) -> bool {
let sections = derive_complexity_options(options);
sections.score || sections.targets
}
pub struct ProgrammaticHealthNextStepFacts {
pub suggestions_enabled: bool,
pub offer_setup: bool,
pub impact_digest: Option<fallow_output::ImpactDigestCounts>,
pub audit_changed: bool,
}
pub struct ProgrammaticHealthAnalysis {
pub report: HealthReport,
pub grouping: Option<HealthGrouping>,
pub root: PathBuf,
pub elapsed: std::time::Duration,
}
impl ProgrammaticHealthAnalysis {
fn from_engine<GroupResolver>(
analysis: fallow_engine::health::HealthAnalysisResult<GroupResolver>,
) -> Self {
Self {
root: analysis.config.root,
report: analysis.report,
grouping: analysis.grouping,
elapsed: analysis.elapsed,
}
}
}
pub struct ProgrammaticHealthRun {
pub analysis: ProgrammaticHealthAnalysis,
pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
pub next_step_facts: ProgrammaticHealthNextStepFacts,
pub telemetry_analysis_run_id: Option<String>,
}
pub trait ProgrammaticHealthRunner {
fn run_programmatic_health(
&self,
options: &ComplexityOptions,
) -> Result<ProgrammaticHealthRun, ProgrammaticError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EngineHealthRunner;
impl ProgrammaticHealthRunner for EngineHealthRunner {
fn run_programmatic_health(
&self,
options: &ComplexityOptions,
) -> Result<ProgrammaticHealthRun, ProgrammaticError> {
let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
resolved.install(|| run_programmatic_health_on_engine(&resolved, options))
}
}
fn run_programmatic_health_on_engine(
resolved: &ProgrammaticAnalysisContext,
options: &ComplexityOptions,
) -> ProgrammaticResult<ProgrammaticHealthRun> {
let health_options = derive_programmatic_health_execution_options(resolved, options);
let result = fallow_engine::health::run_ungrouped_health(
&health_options,
resolved.workspace_roots.clone(),
)
.map_err(|_| generic_health_error("health"))?;
Ok(programmatic_health_run_from_engine_result(result))
}
fn programmatic_health_run_from_engine_result<GroupResolver>(
result: fallow_engine::health::HealthAnalysisResult<GroupResolver>,
) -> ProgrammaticHealthRun {
let root = result.config.root.clone();
let next_step_facts = ProgrammaticHealthNextStepFacts {
suggestions_enabled: suggestions_enabled(),
offer_setup: setup_pointer_applicable(&root),
impact_digest: None,
audit_changed: fallow_engine::churn::is_git_repo(&root),
};
ProgrammaticHealthRun {
workspace_diagnostics: result.workspace_diagnostics.clone(),
analysis: ProgrammaticHealthAnalysis::from_engine(result.without_group_resolver()),
next_step_facts,
telemetry_analysis_run_id: None,
}
}
#[cfg(test)]
pub(super) fn run_health_with_session(
options: &ComplexityOptions,
resolved: &ProgrammaticAnalysisContext,
session: &AnalysisSession,
changed_files: Option<&FxHashSet<PathBuf>>,
) -> ProgrammaticResult<HealthProgrammaticOutput> {
run_health_with_session_artifacts(options, resolved, session, changed_files, None, None)
}
pub(super) fn run_health_with_session_artifacts(
options: &ComplexityOptions,
resolved: &ProgrammaticAnalysisContext,
session: &AnalysisSession,
changed_files: Option<&FxHashSet<PathBuf>>,
pre_computed_analysis: Option<DeadCodeAnalysisArtifacts>,
pre_computed_duplication: Option<DuplicationReport>,
) -> ProgrammaticResult<HealthProgrammaticOutput> {
crate::validate_complexity_options(options)?;
let health_options = derive_programmatic_health_execution_options(resolved, options);
let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
let result = fallow_engine::health::run_ungrouped_health_with_session_artifacts(
&health_options,
workspace_roots,
session,
changed_files.map(|files| files.iter().cloned().collect()),
pre_computed_analysis,
pre_computed_duplication,
)
.map_err(|_| generic_health_error("health"))?;
Ok(assemble_health_programmatic_output(
options,
programmatic_health_run_from_engine_result(result),
))
}
fn generic_health_error(command: &str) -> ProgrammaticError {
let code = format!(
"FALLOW_{}_FAILED",
command.replace('-', "_").to_ascii_uppercase()
);
ProgrammaticError::new(format!("{command} failed"), 2)
.with_code(code)
.with_context(format!("fallow {command}"))
.with_help(format!(
"Re-run `fallow {command} --format json --quiet` in the target project for CLI diagnostics"
))
}
pub fn run_health(options: &ComplexityOptions) -> ProgrammaticResult<HealthProgrammaticOutput> {
run_health_with_runner(options, &EngineHealthRunner)
}
#[must_use]
fn derive_programmatic_health_execution_options<'a>(
resolved: &'a ProgrammaticAnalysisContext,
options: &'a ComplexityOptions,
) -> fallow_engine::health::HealthExecutionOptions<'a> {
let run = crate::derive_complexity_run_options(options);
fallow_engine::health::HealthExecutionOptions {
root: resolved.root(),
config_path: resolved.config_path(),
output: OutputFormat::Human,
no_cache: resolved.no_cache(),
threads: resolved.threads(),
quiet: true,
complexity_breakdown: run.complexity_breakdown,
thresholds: crate::thresholds_to_engine(run.thresholds),
top: run.top,
sort: crate::complexity_sort_to_engine(run.sort),
production: resolved.production_override().unwrap_or(false),
production_override: resolved.production_override(),
changed_since: resolved.changed_since(),
diff_index: resolved.diff_index(),
use_shared_diff_index: false,
workspace: resolved.workspace(),
changed_workspaces: resolved.changed_workspaces(),
baseline: None,
save_baseline: None,
complexity: run.sections.complexity,
file_scores: run.sections.file_scores,
coverage_gaps: run.sections.coverage_gaps,
config_activates_coverage_gaps: !run.sections.any_section,
hotspots: run.sections.hotspots,
ownership: run.sections.ownership,
targets: run.sections.targets,
css: run.css,
css_deep: run.css_deep,
force_full: run.sections.force_full,
score_only_output: run.sections.score_only_output,
enforce_coverage_gap_gate: true,
effort: run.effort.map(crate::target_effort_to_output),
score: run.sections.score,
gates: fallow_engine::health::HealthGateOptions::default(),
since: run.since,
min_commits: run.min_commits,
explain: resolved.explain_enabled(),
summary: false,
save_snapshot: None,
trend: false,
coverage_inputs: crate::coverage_inputs_to_engine(run.coverage_inputs),
performance: false,
runtime_coverage: None,
churn_file: None,
group_by: None,
ownership_emails: run
.ownership_emails
.map(crate::ownership_email_mode_to_config),
}
}
pub fn run_complexity_with_runner(
options: &ComplexityOptions,
runner: &impl ProgrammaticHealthRunner,
) -> ProgrammaticResult<HealthProgrammaticOutput> {
crate::validate_complexity_options(options)?;
Ok(assemble_health_programmatic_output(
options,
runner.run_programmatic_health(options)?,
))
}
fn assemble_health_programmatic_output(
options: &ComplexityOptions,
run: ProgrammaticHealthRun,
) -> HealthProgrammaticOutput {
let ProgrammaticHealthRun {
analysis,
workspace_diagnostics,
next_step_facts,
telemetry_analysis_run_id,
} = run;
let root = analysis.root.clone();
let next_steps =
fallow_output::build_health_next_steps(fallow_output::build_health_next_steps_input(
&analysis.report,
next_step_facts.suggestions_enabled,
next_step_facts.offer_setup,
next_step_facts.impact_digest,
next_step_facts.audit_changed,
));
HealthProgrammaticOutput {
report: analysis.report,
grouping: analysis.grouping,
root,
elapsed: analysis.elapsed,
explain: options.analysis.explain,
workspace_diagnostics,
next_steps,
envelope_mode: root_envelope_mode(),
telemetry_analysis_run_id,
}
}
pub fn run_health_with_runner(
options: &ComplexityOptions,
runner: &impl ProgrammaticHealthRunner,
) -> ProgrammaticResult<HealthProgrammaticOutput> {
run_complexity_with_runner(options, runner)
}
const fn root_envelope_mode() -> RootEnvelopeMode {
RootEnvelopeMode::Tagged
}
#[cfg(test)]
mod tests;