use std::marker::PhantomData;
use std::path::PathBuf;
use anyhow::{Context, Result};
use crate::cli;
use crate::formatting::FormattingConfig;
#[derive(Debug, Clone, Copy)]
pub struct Unvalidated;
#[derive(Debug, Clone, Copy)]
pub struct Validated;
#[derive(Debug, Clone)]
pub struct AnalyzeConfig<State = Unvalidated> {
pub path: PathBuf,
pub format: cli::OutputFormat,
pub output: Option<PathBuf>,
pub threshold_complexity: u32,
pub threshold_duplication: usize,
pub languages: Option<Vec<String>>,
pub coverage_file: Option<PathBuf>,
pub enable_context: bool,
pub context_providers: Option<Vec<String>>,
pub disable_context: Option<Vec<String>>,
pub top: Option<usize>,
pub tail: Option<usize>,
pub summary: bool,
pub semantic_off: bool,
pub verbosity: u8,
pub verbose_macro_warnings: bool,
pub show_macro_stats: bool,
pub group_by_category: bool,
pub min_priority: Option<String>,
pub min_score: Option<f64>,
pub filter_categories: Option<Vec<String>>,
pub no_context_aware: bool,
pub threshold_preset: Option<cli::ThresholdPreset>,
pub _formatting_config: FormattingConfig,
pub parallel: bool,
pub jobs: usize,
pub multi_pass: bool,
pub show_attribution: bool,
pub detail_level: Option<String>,
pub aggregate_only: bool,
pub no_aggregation: bool,
pub aggregation_method: Option<String>,
pub min_problematic: Option<usize>,
pub no_god_object: bool,
pub max_files: Option<usize>,
pub validate_loc: bool,
pub no_public_api_detection: bool,
pub public_api_threshold: f32,
pub no_pattern_detection: bool,
pub patterns: Option<Vec<String>>,
pub pattern_threshold: f32,
pub show_pattern_warnings: bool,
pub debug_call_graph: bool,
pub trace_functions: Option<Vec<String>>,
pub call_graph_stats_only: bool,
pub debug_format: cli::DebugFormatArg,
pub validate_call_graph: bool,
pub show_dependencies: bool,
pub no_dependencies: bool,
pub max_callers: usize,
pub max_callees: usize,
pub show_external: bool,
pub show_std_lib: bool,
pub ast_functional_analysis: bool,
pub functional_analysis_profile: Option<cli::FunctionalAnalysisProfile>,
pub min_split_methods: usize,
pub min_split_lines: usize,
pub no_tui: bool,
pub show_filter_stats: bool,
pub reference_time: chrono::DateTime<chrono::Utc>,
_state: PhantomData<State>,
}
impl AnalyzeConfig<Unvalidated> {
#[allow(clippy::too_many_arguments)]
pub fn new(
path: PathBuf,
format: cli::OutputFormat,
output: Option<PathBuf>,
threshold_complexity: u32,
threshold_duplication: usize,
languages: Option<Vec<String>>,
coverage_file: Option<PathBuf>,
enable_context: bool,
context_providers: Option<Vec<String>>,
disable_context: Option<Vec<String>>,
top: Option<usize>,
tail: Option<usize>,
summary: bool,
semantic_off: bool,
verbosity: u8,
verbose_macro_warnings: bool,
show_macro_stats: bool,
group_by_category: bool,
min_priority: Option<String>,
min_score: Option<f64>,
filter_categories: Option<Vec<String>>,
no_context_aware: bool,
threshold_preset: Option<cli::ThresholdPreset>,
_formatting_config: FormattingConfig,
parallel: bool,
jobs: usize,
multi_pass: bool,
show_attribution: bool,
detail_level: Option<String>,
aggregate_only: bool,
no_aggregation: bool,
aggregation_method: Option<String>,
min_problematic: Option<usize>,
no_god_object: bool,
max_files: Option<usize>,
validate_loc: bool,
no_public_api_detection: bool,
public_api_threshold: f32,
no_pattern_detection: bool,
patterns: Option<Vec<String>>,
pattern_threshold: f32,
show_pattern_warnings: bool,
debug_call_graph: bool,
trace_functions: Option<Vec<String>>,
call_graph_stats_only: bool,
debug_format: cli::DebugFormatArg,
validate_call_graph: bool,
show_dependencies: bool,
no_dependencies: bool,
max_callers: usize,
max_callees: usize,
show_external: bool,
show_std_lib: bool,
ast_functional_analysis: bool,
functional_analysis_profile: Option<cli::FunctionalAnalysisProfile>,
min_split_methods: usize,
min_split_lines: usize,
no_tui: bool,
show_filter_stats: bool,
reference_time: chrono::DateTime<chrono::Utc>,
) -> Self {
AnalyzeConfig {
path,
format,
output,
threshold_complexity,
threshold_duplication,
languages,
coverage_file,
enable_context,
context_providers,
disable_context,
top,
tail,
summary,
semantic_off,
verbosity,
verbose_macro_warnings,
show_macro_stats,
group_by_category,
min_priority,
min_score,
filter_categories,
no_context_aware,
threshold_preset,
_formatting_config,
parallel,
jobs,
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,
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,
no_tui,
show_filter_stats,
reference_time,
_state: PhantomData,
}
}
pub fn validate(self) -> Result<AnalyzeConfig<Validated>> {
if !self.path.exists() {
anyhow::bail!("Path does not exist: {}", self.path.display());
}
if self.threshold_complexity == 0 {
anyhow::bail!("Complexity threshold must be greater than 0");
}
if let Some(ref output) = self.output {
if let Some(parent) = output.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
anyhow::bail!("Output directory does not exist: {}", parent.display());
}
}
}
if let Some(ref coverage_file) = self.coverage_file {
if !coverage_file.exists() {
anyhow::bail!("Coverage file does not exist: {}", coverage_file.display());
}
}
if self.jobs == 0 && self.parallel {
anyhow::bail!("Job count must be greater than 0 when parallel is enabled");
}
if let Some(min_score) = self.min_score {
if !(0.0..=100.0).contains(&min_score) {
anyhow::bail!("Minimum score must be between 0.0 and 100.0");
}
}
Ok(AnalyzeConfig {
path: self.path,
format: self.format,
output: self.output,
threshold_complexity: self.threshold_complexity,
threshold_duplication: self.threshold_duplication,
languages: self.languages,
coverage_file: self.coverage_file,
enable_context: self.enable_context,
context_providers: self.context_providers,
disable_context: self.disable_context,
top: self.top,
tail: self.tail,
summary: self.summary,
semantic_off: self.semantic_off,
verbosity: self.verbosity,
verbose_macro_warnings: self.verbose_macro_warnings,
show_macro_stats: self.show_macro_stats,
group_by_category: self.group_by_category,
min_priority: self.min_priority,
min_score: self.min_score,
filter_categories: self.filter_categories,
no_context_aware: self.no_context_aware,
threshold_preset: self.threshold_preset,
_formatting_config: self._formatting_config,
parallel: self.parallel,
jobs: self.jobs,
multi_pass: self.multi_pass,
show_attribution: self.show_attribution,
detail_level: self.detail_level,
aggregate_only: self.aggregate_only,
no_aggregation: self.no_aggregation,
aggregation_method: self.aggregation_method,
min_problematic: self.min_problematic,
no_god_object: self.no_god_object,
max_files: self.max_files,
validate_loc: self.validate_loc,
no_public_api_detection: self.no_public_api_detection,
public_api_threshold: self.public_api_threshold,
no_pattern_detection: self.no_pattern_detection,
patterns: self.patterns,
pattern_threshold: self.pattern_threshold,
show_pattern_warnings: self.show_pattern_warnings,
debug_call_graph: self.debug_call_graph,
trace_functions: self.trace_functions,
call_graph_stats_only: self.call_graph_stats_only,
debug_format: self.debug_format,
validate_call_graph: self.validate_call_graph,
show_dependencies: self.show_dependencies,
no_dependencies: self.no_dependencies,
max_callers: self.max_callers,
max_callees: self.max_callees,
show_external: self.show_external,
show_std_lib: self.show_std_lib,
ast_functional_analysis: self.ast_functional_analysis,
functional_analysis_profile: self.functional_analysis_profile,
min_split_methods: self.min_split_methods,
min_split_lines: self.min_split_lines,
no_tui: self.no_tui,
show_filter_stats: self.show_filter_stats,
reference_time: self.reference_time,
_state: PhantomData,
})
}
}
impl AnalyzeConfig<Validated> {
pub fn execute(self) -> Result<()> {
let old_config = super::analyze::AnalyzeConfig {
path: self.path,
format: self.format,
output: self.output,
threshold_complexity: self.threshold_complexity,
threshold_duplication: self.threshold_duplication,
languages: self.languages,
coverage_file: self.coverage_file,
enable_context: self.enable_context,
context_providers: self.context_providers,
disable_context: self.disable_context,
top: self.top,
tail: self.tail,
summary: self.summary,
semantic_off: self.semantic_off,
verbosity: self.verbosity,
verbose_macro_warnings: self.verbose_macro_warnings,
show_macro_stats: self.show_macro_stats,
group_by_category: self.group_by_category,
min_priority: self.min_priority,
min_score: self.min_score,
filter_categories: self.filter_categories,
no_context_aware: self.no_context_aware,
threshold_preset: self.threshold_preset,
_formatting_config: self._formatting_config,
parallel: self.parallel,
jobs: self.jobs,
multi_pass: self.multi_pass,
show_attribution: self.show_attribution,
detail_level: self.detail_level,
aggregate_only: self.aggregate_only,
no_aggregation: self.no_aggregation,
aggregation_method: self.aggregation_method,
min_problematic: self.min_problematic,
no_god_object: self.no_god_object,
max_files: self.max_files,
validate_loc: self.validate_loc,
no_public_api_detection: self.no_public_api_detection,
public_api_threshold: self.public_api_threshold,
no_pattern_detection: self.no_pattern_detection,
patterns: self.patterns,
pattern_threshold: self.pattern_threshold,
show_pattern_warnings: self.show_pattern_warnings,
debug_call_graph: self.debug_call_graph,
trace_functions: self.trace_functions,
call_graph_stats_only: self.call_graph_stats_only,
debug_format: self.debug_format,
validate_call_graph: self.validate_call_graph,
show_dependencies: self.show_dependencies,
no_dependencies: self.no_dependencies,
max_callers: self.max_callers,
max_callees: self.max_callees,
show_external: self.show_external,
show_std_lib: self.show_std_lib,
ast_functional_analysis: self.ast_functional_analysis,
functional_analysis_profile: self.functional_analysis_profile,
min_split_methods: self.min_split_methods,
min_split_lines: self.min_split_lines,
no_tui: self.no_tui,
show_filter_stats: self.show_filter_stats,
reference_time: self.reference_time,
};
super::analyze::handle_analyze(old_config).context("Analysis execution failed")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_transition() {
let config = AnalyzeConfig::new(
PathBuf::from("."),
cli::OutputFormat::Terminal,
None,
10,
5,
None,
None,
false,
None,
None,
None,
None,
false,
false,
0,
false,
false,
false,
None,
None,
None,
false,
None,
FormattingConfig::default(),
true,
4,
false,
false,
None,
false,
false,
None,
None,
false,
None,
false,
false,
0.5,
false,
None,
0.5,
false,
false,
None,
false,
cli::DebugFormatArg::Text,
false,
false,
false,
100,
100,
false,
false,
false,
None,
3,
20,
false,
false,
chrono::Utc::now(),
);
let validated = config.validate();
assert!(validated.is_ok());
}
#[test]
fn test_validation_fails_for_nonexistent_path() {
let config = AnalyzeConfig::new(
PathBuf::from("/nonexistent/path/that/should/not/exist"),
cli::OutputFormat::Terminal,
None,
10,
5,
None,
None,
false,
None,
None,
None,
None,
false,
false,
0,
false,
false,
false,
None,
None,
None,
false,
None,
FormattingConfig::default(),
true,
4,
false,
false,
None,
false,
false,
None,
None,
false,
None,
false,
false,
0.5,
false,
None,
0.5,
false,
false,
None,
false,
cli::DebugFormatArg::Text,
false,
false,
false,
100,
100,
false,
false,
false,
None,
3,
20,
false,
false,
chrono::Utc::now(),
);
let result = config.validate();
assert!(result.is_err());
}
#[test]
fn test_validation_fails_for_zero_complexity() {
let config = AnalyzeConfig::new(
PathBuf::from("."),
cli::OutputFormat::Terminal,
None,
0, 5,
None,
None,
false,
None,
None,
None,
None,
false,
false,
0,
false,
false,
false,
None,
None,
None,
false,
None,
FormattingConfig::default(),
true,
4,
false,
false,
None,
false,
false,
None,
None,
false,
None,
false,
false,
0.5,
false,
None,
0.5,
false,
false,
None,
false,
cli::DebugFormatArg::Text,
false,
false,
false,
100,
100,
false,
false,
false,
None,
3,
20,
false,
false,
chrono::Utc::now(),
);
let result = config.validate();
assert!(result.is_err());
}
#[test]
fn test_validation_fails_for_invalid_score() {
let config = AnalyzeConfig::new(
PathBuf::from("."),
cli::OutputFormat::Terminal,
None,
10,
5,
None,
None,
false,
None,
None,
None,
None,
false,
false,
0,
false,
false,
false,
None,
Some(150.0), None,
false,
None,
FormattingConfig::default(),
true,
4,
false,
false,
None,
false,
false,
None,
None,
false,
None,
false,
false,
0.5,
false,
None,
0.5,
false,
false,
None,
false,
cli::DebugFormatArg::Text,
false,
false,
false,
100,
100,
false,
false,
false,
None,
3,
20,
false,
false,
chrono::Utc::now(),
);
let result = config.validate();
assert!(result.is_err());
}
}