use std::path::{Path, PathBuf};
use super::analysis_config::{AnalysisConfig, AnalysisConfigBuilder};
use super::multi_source::ConfigSource;
use crate::errors::AnalysisError;
pub struct CliValidationResult {
pub config: Option<AnalysisConfig>,
pub errors: Vec<CliValidationError>,
}
#[derive(Debug, Clone)]
pub struct CliValidationError {
pub argument: String,
pub message: String,
pub suggestion: Option<String>,
}
impl std::fmt::Display for CliValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[cli:{}] {}", self.argument, self.message)?;
if let Some(ref suggestion) = self.suggestion {
write!(f, "\n suggestion: {}", suggestion)?;
}
Ok(())
}
}
impl From<CliValidationError> for AnalysisError {
fn from(err: CliValidationError) -> Self {
AnalysisError::config(err.to_string())
}
}
#[allow(clippy::too_many_arguments)]
pub fn validate_analyze_args(
path: &Path,
aggregate_only: bool,
no_aggregation: bool,
jobs: usize,
no_parallel: bool,
coverage_file: Option<&Path>,
enable_context: bool,
no_multi_pass: bool,
) -> Vec<CliValidationError> {
let mut errors = Vec::new();
if !path.exists() {
errors.push(CliValidationError {
argument: "PATH".to_string(),
message: format!("directory does not exist: {}", path.display()),
suggestion: Some("Check that the path is correct and accessible".to_string()),
});
} else if !path.is_dir() {
errors.push(CliValidationError {
argument: "PATH".to_string(),
message: format!("path is not a directory: {}", path.display()),
suggestion: Some("Provide a directory path, not a file".to_string()),
});
}
if aggregate_only && no_aggregation {
errors.push(CliValidationError {
argument: "--aggregate-only".to_string(),
message: "cannot use with --no-aggregation".to_string(),
suggestion: Some("Remove one of these options".to_string()),
});
errors.push(CliValidationError {
argument: "--no-aggregation".to_string(),
message: "cannot use with --aggregate-only".to_string(),
suggestion: Some("Remove one of these options".to_string()),
});
}
if !no_parallel && jobs == 0 {
}
if jobs > 256 {
errors.push(CliValidationError {
argument: "--jobs".to_string(),
message: format!("value {} is too large (max 256)", jobs),
suggestion: Some("Use a value between 1 and 256".to_string()),
});
}
if let Some(coverage_path) = coverage_file {
if !coverage_path.exists() {
errors.push(CliValidationError {
argument: "--coverage-file".to_string(),
message: format!("file does not exist: {}", coverage_path.display()),
suggestion: Some("Check that the coverage file path is correct".to_string()),
});
}
}
if !enable_context && !no_multi_pass {
}
errors
}
#[allow(clippy::too_many_arguments)]
pub fn build_analysis_config_from_cli(
path: PathBuf,
aggregate_only: bool,
no_aggregation: bool,
jobs: usize,
no_parallel: bool,
coverage_file: Option<PathBuf>,
enable_context: bool,
no_multi_pass: bool,
complexity_threshold: u32,
exclude_patterns: Vec<String>,
show_config_sources: bool,
) -> Result<AnalysisConfig, Vec<AnalysisError>> {
let parallel = !no_parallel;
let multi_pass = enable_context && !no_multi_pass;
let effective_jobs = if parallel && jobs == 0 {
std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(4)
} else {
jobs
};
let builder = AnalysisConfigBuilder::new(path)
.parallel_from(
parallel,
ConfigSource::Environment("--no-parallel".to_string()),
)
.jobs_from(
effective_jobs,
ConfigSource::Environment("--jobs".to_string()),
)
.aggregate_only(aggregate_only)
.no_aggregation(no_aggregation)
.coverage_file_from(
coverage_file,
ConfigSource::Environment("--coverage-file".to_string()),
)
.enable_context(enable_context)
.multi_pass(multi_pass)
.complexity_threshold(complexity_threshold)
.exclude_patterns(exclude_patterns)
.show_config_sources(show_config_sources);
builder.build()
}
pub fn format_cli_errors(errors: &[CliValidationError]) -> String {
if errors.is_empty() {
return String::new();
}
let mut output = format!("CLI argument errors ({}):\n", errors.len());
for error in errors {
output.push_str(&format!("\n {}\n", error));
}
output.push_str("\nFix all errors and try again.\n");
output
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_valid_args_no_errors() {
let temp_dir = TempDir::new().unwrap();
let errors = validate_analyze_args(
temp_dir.path(),
false, false, 4, false, None, false, true, );
assert!(errors.is_empty());
}
#[test]
fn test_mutual_exclusion_error() {
let temp_dir = TempDir::new().unwrap();
let errors = validate_analyze_args(
temp_dir.path(),
true, true, 4,
false,
None,
false,
true,
);
assert_eq!(errors.len(), 2); assert!(errors[0].argument.contains("aggregate"));
assert!(errors[1].argument.contains("aggregation"));
}
#[test]
fn test_missing_path_error() {
let errors = validate_analyze_args(
&PathBuf::from("/nonexistent/path/that/does/not/exist"),
false,
false,
4,
false,
None,
false,
true,
);
assert!(!errors.is_empty());
assert!(errors[0].argument == "PATH");
assert!(errors[0].message.contains("does not exist"));
}
#[test]
fn test_missing_coverage_file_error() {
let temp_dir = TempDir::new().unwrap();
let missing_file = PathBuf::from("/nonexistent/coverage.lcov");
let errors = validate_analyze_args(
temp_dir.path(),
false,
false,
4,
false,
Some(missing_file.as_path()),
false,
true,
);
assert!(!errors.is_empty());
assert!(errors
.iter()
.any(|e| e.argument == "--coverage-file" && e.message.contains("does not exist")));
}
#[test]
fn test_jobs_too_high_error() {
let temp_dir = TempDir::new().unwrap();
let errors = validate_analyze_args(
temp_dir.path(),
false,
false,
500, false,
None,
false,
true,
);
assert!(!errors.is_empty());
assert!(errors.iter().any(|e| e.argument == "--jobs"));
}
#[test]
fn test_format_cli_errors() {
let errors = vec![CliValidationError {
argument: "--test".to_string(),
message: "test error".to_string(),
suggestion: Some("fix it".to_string()),
}];
let output = format_cli_errors(&errors);
assert!(output.contains("CLI argument errors (1)"));
assert!(output.contains("--test"));
assert!(output.contains("test error"));
assert!(output.contains("fix it"));
}
#[test]
fn test_build_analysis_config_valid() {
let temp_dir = TempDir::new().unwrap();
let result = build_analysis_config_from_cli(
temp_dir.path().to_path_buf(),
false,
false,
4,
false,
None,
false,
true,
50,
Vec::new(),
false,
);
assert!(result.is_ok());
let config = result.unwrap();
assert!(config.parallel);
assert_eq!(config.jobs, 4);
}
#[test]
fn test_build_analysis_config_with_errors() {
let result = build_analysis_config_from_cli(
PathBuf::from("/nonexistent"),
true, true, 0, false, None,
true, false, 0, vec!["[invalid".to_string()], false,
);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.len() >= 2);
}
#[test]
fn test_auto_detect_jobs() {
let temp_dir = TempDir::new().unwrap();
let result = build_analysis_config_from_cli(
temp_dir.path().to_path_buf(),
false,
false,
0, false, None,
false,
true,
50,
Vec::new(),
false,
);
assert!(result.is_ok());
let config = result.unwrap();
assert!(config.jobs > 0);
}
}