use crate::cli::args::Commands;
use crate::observability::{enable_profiling, get_timing_report};
use std::path::PathBuf;
use crate::cli::config_builder::{
build_debug_config, build_display_config, build_feature_config, build_language_config,
build_path_config, build_performance_config, build_threshold_config, compute_multi_pass,
compute_verbosity, convert_context_providers, convert_disable_context,
convert_filter_categories, convert_languages, convert_min_priority, convert_output_format,
convert_threshold_preset, create_formatting_config, should_use_parallel, AnalysisFeatureConfig,
DebugConfig, DisplayConfig, LanguageConfig, PathConfig, PerformanceConfig, ThresholdConfig,
};
use crate::cli::setup::{apply_environment_setup, get_worker_count, print_metrics_explanation};
use crate::error::{CliError, ConfigError};
use anyhow::Result;
#[allow(clippy::type_complexity)]
pub fn extract_analyze_params(
command: Commands,
) -> Result<(
PathConfig,
ThresholdConfig,
AnalysisFeatureConfig,
DisplayConfig,
PerformanceConfig,
DebugConfig,
LanguageConfig,
bool, // explain_metrics flag
bool, // no_context_aware flag
)> {
if let Commands::Analyze {
path,
format,
output,
threshold_complexity,
threshold_duplication,
languages,
coverage_file,
enable_context,
context_providers,
disable_context,
top,
tail,
summary,
semantic_off,
explain_score: _,
verbosity,
compact,
verbose_macro_warnings,
show_macro_stats,
group_by_category,
min_priority,
min_score,
filter_categories,
no_context_aware,
threshold_preset,
plain,
no_parallel,
jobs,
no_multi_pass,
show_attribution,
detail_level,
aggregate_only,
no_aggregation,
aggregation_method,
min_problematic,
no_god_object,
max_files,
validate_loc,
no_public_api_detection,
public_api_threshold,
no_pattern_detection,
patterns,
pattern_threshold,
show_pattern_warnings,
explain_metrics,
debug_call_graph,
trace_functions,
call_graph_stats_only,
debug_format,
validate_call_graph,
show_dependencies,
no_dependencies,
max_callers,
max_callees,
show_external,
show_std_lib,
ast_functional_analysis,
functional_analysis_profile,
min_split_methods,
min_split_lines,
show_splits,
no_tui,
quiet: _,
streaming: _,
stream_to: _,
show_filter_stats,
profile: _,
profile_output: _,
} = command
{
let path_cfg = build_path_config(
path,
output,
coverage_file,
max_files,
min_priority,
min_score,
filter_categories,
min_problematic,
);
let threshold_cfg = build_threshold_config(
threshold_complexity,
threshold_duplication,
threshold_preset,
public_api_threshold,
);
let feature_cfg = build_feature_config(
enable_context,
context_providers,
disable_context,
semantic_off,
no_pattern_detection,
patterns,
pattern_threshold,
no_god_object,
no_public_api_detection,
ast_functional_analysis,
functional_analysis_profile,
min_split_methods,
min_split_lines,
validate_loc,
validate_call_graph,
);
let formatting_config = create_formatting_config(
plain,
show_dependencies,
no_dependencies,
max_callers,
max_callees,
show_external,
show_std_lib,
show_splits,
);
let display_cfg = build_display_config(
format,
compute_verbosity(verbosity, compact),
summary,
top,
tail,
group_by_category,
show_attribution,
detail_level,
no_tui,
show_filter_stats,
formatting_config,
no_context_aware,
);
let perf_cfg = build_performance_config(
should_use_parallel(no_parallel),
get_worker_count(jobs),
compute_multi_pass(no_multi_pass),
aggregate_only,
no_aggregation,
);
let debug_cfg = build_debug_config(
verbose_macro_warnings,
show_macro_stats,
debug_call_graph,
trace_functions,
call_graph_stats_only,
debug_format,
show_pattern_warnings,
show_dependencies,
no_dependencies,
);
let lang_cfg = build_language_config(
languages,
aggregation_method,
max_callers,
max_callees,
show_external,
show_std_lib,
);
Ok((
path_cfg,
threshold_cfg,
feature_cfg,
display_cfg,
perf_cfg,
debug_cfg,
lang_cfg,
explain_metrics,
no_context_aware,
))
} else {
Err(anyhow::anyhow!("Invalid command: expected Analyze variant"))
}
}
pub fn handle_analyze_command(command: Commands) -> Result<(), CliError> {
let (
path_cfg,
threshold_cfg,
feature_cfg,
display_cfg,
perf_cfg,
debug_cfg,
lang_cfg,
explain_metrics,
no_context_aware,
) = extract_analyze_params(command).map_err(|e| CliError::InvalidCommand(e.to_string()))?;
apply_environment_setup(no_context_aware)
.map_err(|e| CliError::Config(ConfigError::ValidationFailed(e.to_string())))?;
if explain_metrics {
print_metrics_explanation();
return Ok(());
}
let unvalidated_config = build_analyze_config(
path_cfg,
threshold_cfg,
feature_cfg,
display_cfg,
perf_cfg,
debug_cfg,
lang_cfg,
);
let validated_config = unvalidated_config
.validate()
.map_err(|e| CliError::Config(ConfigError::ValidationFailed(e.to_string())))?;
validated_config
.execute()
.map_err(|e| CliError::Config(ConfigError::ValidationFailed(e.to_string())))
}
pub fn handle_analyze_command_with_profiling(command: Commands) -> Result<(), CliError> {
let (profile, profile_output) = extract_profiling_options(&command);
if profile {
enable_profiling();
}
handle_analyze_command(command)?;
if profile {
output_profiling_report(profile_output)
.map_err(|e| CliError::InvalidCommand(e.to_string()))?;
}
Ok(())
}
fn extract_profiling_options(command: &Commands) -> (bool, Option<PathBuf>) {
if let Commands::Analyze {
profile,
profile_output,
..
} = command
{
(*profile, profile_output.clone())
} else {
(false, None)
}
}
fn output_profiling_report(output_path: Option<PathBuf>) -> Result<()> {
let report = get_timing_report();
match output_path {
Some(path) => {
std::fs::write(&path, report.to_json())
.map_err(|e| anyhow::anyhow!("Failed to write profile output: {}", e))?;
eprintln!("Profiling data written to: {}", path.display());
}
None => {
eprintln!("{}", report.to_summary());
}
}
Ok(())
}
fn build_analyze_config(
p: PathConfig,
t: ThresholdConfig,
f: AnalysisFeatureConfig,
d: DisplayConfig,
pf: PerformanceConfig,
db: DebugConfig,
l: LanguageConfig,
) -> crate::commands::AnalyzeConfig<crate::commands::Unvalidated> {
crate::commands::AnalyzeConfig::new(
p.path,
convert_output_format(d.format),
p.output,
t.complexity,
t.duplication,
convert_languages(l.languages),
p.coverage_file,
f.enable_context,
convert_context_providers(f.context_providers),
convert_disable_context(f.disable_context),
d.top,
d.tail,
d.summary,
f.semantic_off,
d.verbosity,
db.verbose_macro_warnings,
db.show_macro_stats,
d.group_by_category,
convert_min_priority(p.min_priority),
p.min_score,
convert_filter_categories(p.filter_categories),
d.no_context_aware,
convert_threshold_preset(t.preset),
d.formatting_config,
pf.parallel,
pf.jobs,
pf.multi_pass,
d.show_attribution,
d.detail_level,
pf.aggregate_only,
pf.no_aggregation,
l.aggregation_method,
p.min_problematic,
f.no_god_object,
p.max_files,
f.validate_loc,
f.no_public_api_detection,
t.public_api_threshold,
f.no_pattern_detection,
f.patterns,
f.pattern_threshold,
db.show_pattern_warnings,
db.debug_call_graph,
db.trace_functions,
db.call_graph_stats_only,
db.debug_format,
f.validate_call_graph,
db.show_dependencies,
db.no_dependencies,
l.max_callers,
l.max_callees,
l.show_external,
l.show_std_lib,
f.ast_functional_analysis,
f.functional_analysis_profile,
f.min_split_methods,
f.min_split_lines,
d.no_tui,
d.show_filter_stats,
chrono::Utc::now(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::CliError;
#[test]
fn test_error_mapping_cli_to_app() {
let cli_error = CliError::InvalidCommand("test error".to_string());
let error_string = format!("{}", cli_error);
assert!(error_string.contains("test error"));
}
#[test]
fn test_handler_composition_with_errors() {
let analyze_result: Result<(), anyhow::Error> = Err(anyhow::anyhow!("analysis failed"));
let cli_result: Result<(), CliError> = analyze_result
.map_err(|e| CliError::Config(ConfigError::ValidationFailed(e.to_string())));
assert!(cli_result.is_err());
match cli_result.unwrap_err() {
CliError::Config(ConfigError::ValidationFailed(msg)) => {
assert!(msg.contains("analysis failed"));
}
_ => panic!("Wrong error type"),
}
}
#[test]
fn test_pipeline_error_handling() {
let result = Ok::<i32, String>(42)
.map(|x| x * 2)
.map_err(|e| format!("Stage 1: {}", e))
.and_then(|x| {
if x > 50 {
Ok(x)
} else {
Err("Too small".to_string())
}
})
.map_err(|e| format!("Stage 2: {}", e));
assert!(result.is_ok());
assert_eq!(result.unwrap(), 84);
}
#[test]
fn test_error_context_preservation() {
use anyhow::Context;
let result: Result<(), anyhow::Error> = Err(anyhow::anyhow!("root cause"))
.context("Operation failed")
.context("Command failed");
let cli_result: Result<(), CliError> =
result.map_err(|e| CliError::Config(ConfigError::ValidationFailed(format!("{:#}", e))));
assert!(cli_result.is_err());
let error_msg = format!("{}", cli_result.unwrap_err());
assert!(error_msg.contains("Command failed"));
}
#[test]
fn test_extract_profiling_options_from_analyze() {
use crate::cli::Cli;
use clap::Parser;
let cli = Cli::parse_from(["debtmap", "analyze", ".", "--profile"]);
let (profile, profile_output) = extract_profiling_options(&cli.command);
assert!(profile);
assert!(profile_output.is_none());
}
#[test]
fn test_extract_profiling_options_with_output() {
use crate::cli::Cli;
use clap::Parser;
let cli = Cli::parse_from([
"debtmap",
"analyze",
".",
"--profile",
"--profile-output",
"output.json",
]);
let (profile, profile_output) = extract_profiling_options(&cli.command);
assert!(profile);
assert_eq!(profile_output, Some(PathBuf::from("output.json")));
}
#[test]
fn test_extract_profiling_options_no_profile() {
use crate::cli::Cli;
use clap::Parser;
let cli = Cli::parse_from(["debtmap", "analyze", "."]);
let (profile, profile_output) = extract_profiling_options(&cli.command);
assert!(!profile);
assert!(profile_output.is_none());
}
#[test]
fn test_extract_profiling_options_non_analyze_command() {
use crate::cli::Cli;
use clap::Parser;
let cli = Cli::parse_from(["debtmap", "init"]);
let (profile, profile_output) = extract_profiling_options(&cli.command);
assert!(!profile);
assert!(profile_output.is_none());
}
#[test]
fn test_output_profiling_report_to_file() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let output_path = dir.path().join("profile.json");
let result = output_profiling_report(Some(output_path.clone()));
assert!(result.is_ok());
assert!(output_path.exists());
let content = std::fs::read_to_string(&output_path).unwrap();
assert!(
content.contains("{") || content.contains("timing"),
"Expected JSON output, got: {}",
content
);
}
#[test]
fn test_output_profiling_report_to_stderr() {
let result = output_profiling_report(None);
assert!(result.is_ok());
}
}