1use std::marker::PhantomData;
24use std::path::PathBuf;
25
26use anyhow::{Context, Result};
27
28use crate::cli;
29use crate::formatting::FormattingConfig;
30
31#[derive(Debug, Clone, Copy)]
33pub struct Unvalidated;
34
35#[derive(Debug, Clone, Copy)]
37pub struct Validated;
38
39#[derive(Debug, Clone)]
44pub struct AnalyzeConfig<State = Unvalidated> {
45 pub path: PathBuf,
47 pub format: cli::OutputFormat,
48 pub output: Option<PathBuf>,
49 pub threshold_complexity: u32,
50 pub threshold_duplication: usize,
51 pub languages: Option<Vec<String>>,
52 pub coverage_file: Option<PathBuf>,
53 pub enable_context: bool,
54 pub context_providers: Option<Vec<String>>,
55 pub disable_context: Option<Vec<String>>,
56 pub top: Option<usize>,
57 pub tail: Option<usize>,
58 pub summary: bool,
59 pub semantic_off: bool,
60 pub verbosity: u8,
61 pub verbose_macro_warnings: bool,
62 pub show_macro_stats: bool,
63 pub min_priority: Option<String>,
64 pub min_score: Option<f64>,
65 pub filter_categories: Option<Vec<String>>,
66 pub no_context_aware: bool,
67 pub threshold_preset: Option<cli::ThresholdPreset>,
68 pub _formatting_config: FormattingConfig,
69 pub parallel: bool,
70 pub jobs: usize,
71 pub multi_pass: bool,
72 pub show_attribution: bool,
73 pub aggregate_only: bool,
74 pub no_aggregation: bool,
75 pub aggregation_method: Option<String>,
76 pub min_problematic: Option<usize>,
77 pub no_god_object: bool,
78 pub max_files: Option<usize>,
79 pub debug_call_graph: bool,
80 pub trace_functions: Option<Vec<String>>,
81 pub call_graph_stats_only: bool,
82 pub debug_format: cli::DebugFormatArg,
83 pub validate_call_graph: bool,
84 pub ast_functional_analysis: bool,
85 pub functional_analysis_profile: Option<cli::FunctionalAnalysisProfile>,
86 pub no_tui: bool,
87 pub show_filter_stats: bool,
88 pub reference_time: chrono::DateTime<chrono::Utc>,
89
90 _state: PhantomData<State>,
92}
93
94impl AnalyzeConfig<Unvalidated> {
95 #[allow(clippy::too_many_arguments)]
97 pub fn new(
98 path: PathBuf,
99 format: cli::OutputFormat,
100 output: Option<PathBuf>,
101 threshold_complexity: u32,
102 threshold_duplication: usize,
103 languages: Option<Vec<String>>,
104 coverage_file: Option<PathBuf>,
105 enable_context: bool,
106 context_providers: Option<Vec<String>>,
107 disable_context: Option<Vec<String>>,
108 top: Option<usize>,
109 tail: Option<usize>,
110 summary: bool,
111 semantic_off: bool,
112 verbosity: u8,
113 verbose_macro_warnings: bool,
114 show_macro_stats: bool,
115 min_priority: Option<String>,
116 min_score: Option<f64>,
117 filter_categories: Option<Vec<String>>,
118 no_context_aware: bool,
119 threshold_preset: Option<cli::ThresholdPreset>,
120 _formatting_config: FormattingConfig,
121 parallel: bool,
122 jobs: usize,
123 multi_pass: bool,
124 show_attribution: bool,
125 aggregate_only: bool,
126 no_aggregation: bool,
127 aggregation_method: Option<String>,
128 min_problematic: Option<usize>,
129 no_god_object: bool,
130 max_files: Option<usize>,
131 debug_call_graph: bool,
132 trace_functions: Option<Vec<String>>,
133 call_graph_stats_only: bool,
134 debug_format: cli::DebugFormatArg,
135 validate_call_graph: bool,
136 ast_functional_analysis: bool,
137 functional_analysis_profile: Option<cli::FunctionalAnalysisProfile>,
138 no_tui: bool,
139 show_filter_stats: bool,
140 reference_time: chrono::DateTime<chrono::Utc>,
141 ) -> Self {
142 AnalyzeConfig {
143 path,
144 format,
145 output,
146 threshold_complexity,
147 threshold_duplication,
148 languages,
149 coverage_file,
150 enable_context,
151 context_providers,
152 disable_context,
153 top,
154 tail,
155 summary,
156 semantic_off,
157 verbosity,
158 verbose_macro_warnings,
159 show_macro_stats,
160 min_priority,
161 min_score,
162 filter_categories,
163 no_context_aware,
164 threshold_preset,
165 _formatting_config,
166 parallel,
167 jobs,
168 multi_pass,
169 show_attribution,
170 aggregate_only,
171 no_aggregation,
172 aggregation_method,
173 min_problematic,
174 no_god_object,
175 max_files,
176 debug_call_graph,
177 trace_functions,
178 call_graph_stats_only,
179 debug_format,
180 validate_call_graph,
181 ast_functional_analysis,
182 functional_analysis_profile,
183 no_tui,
184 show_filter_stats,
185 reference_time,
186 _state: PhantomData,
187 }
188 }
189
190 pub fn validate(self) -> Result<AnalyzeConfig<Validated>> {
196 if !self.path.exists() {
198 anyhow::bail!("Path does not exist: {}", self.path.display());
199 }
200
201 if self.threshold_complexity == 0 {
203 anyhow::bail!("Complexity threshold must be greater than 0");
204 }
205
206 if let Some(ref output) = self.output
208 && let Some(parent) = output.parent()
209 {
210 if !parent.as_os_str().is_empty() && !parent.exists() {
213 anyhow::bail!("Output directory does not exist: {}", parent.display());
214 }
215 }
216
217 if let Some(ref coverage_file) = self.coverage_file
219 && !coverage_file.exists()
220 {
221 anyhow::bail!("Coverage file does not exist: {}", coverage_file.display());
222 }
223
224 if self.jobs == 0 && self.parallel {
226 anyhow::bail!("Job count must be greater than 0 when parallel is enabled");
227 }
228
229 if let Some(min_score) = self.min_score
231 && !(0.0..=100.0).contains(&min_score)
232 {
233 anyhow::bail!("Minimum score must be between 0.0 and 100.0");
234 }
235
236 if let Some(ref min_priority) = self.min_priority
237 && !matches!(
238 min_priority.to_ascii_lowercase().as_str(),
239 "low" | "medium" | "high" | "critical"
240 )
241 {
242 anyhow::bail!("Minimum priority must be one of: low, medium, high, critical");
243 }
244
245 Ok(AnalyzeConfig {
247 path: self.path,
248 format: self.format,
249 output: self.output,
250 threshold_complexity: self.threshold_complexity,
251 threshold_duplication: self.threshold_duplication,
252 languages: self.languages,
253 coverage_file: self.coverage_file,
254 enable_context: self.enable_context,
255 context_providers: self.context_providers,
256 disable_context: self.disable_context,
257 top: self.top,
258 tail: self.tail,
259 summary: self.summary,
260 semantic_off: self.semantic_off,
261 verbosity: self.verbosity,
262 verbose_macro_warnings: self.verbose_macro_warnings,
263 show_macro_stats: self.show_macro_stats,
264 min_priority: self.min_priority,
265 min_score: self.min_score,
266 filter_categories: self.filter_categories,
267 no_context_aware: self.no_context_aware,
268 threshold_preset: self.threshold_preset,
269 _formatting_config: self._formatting_config,
270 parallel: self.parallel,
271 jobs: self.jobs,
272 multi_pass: self.multi_pass,
273 show_attribution: self.show_attribution,
274 aggregate_only: self.aggregate_only,
275 no_aggregation: self.no_aggregation,
276 aggregation_method: self.aggregation_method,
277 min_problematic: self.min_problematic,
278 no_god_object: self.no_god_object,
279 max_files: self.max_files,
280 debug_call_graph: self.debug_call_graph,
281 trace_functions: self.trace_functions,
282 call_graph_stats_only: self.call_graph_stats_only,
283 debug_format: self.debug_format,
284 validate_call_graph: self.validate_call_graph,
285 ast_functional_analysis: self.ast_functional_analysis,
286 functional_analysis_profile: self.functional_analysis_profile,
287 no_tui: self.no_tui,
288 show_filter_stats: self.show_filter_stats,
289 reference_time: self.reference_time,
290 _state: PhantomData,
291 })
292 }
293}
294
295impl AnalyzeConfig<Validated> {
296 pub fn execute(self) -> Result<()> {
301 let old_config = super::analyze::AnalyzeConfig {
303 path: self.path,
304 format: self.format,
305 output: self.output,
306 threshold_complexity: self.threshold_complexity,
307 threshold_duplication: self.threshold_duplication,
308 languages: self.languages,
309 coverage_file: self.coverage_file,
310 enable_context: self.enable_context,
311 context_providers: self.context_providers,
312 disable_context: self.disable_context,
313 top: self.top,
314 tail: self.tail,
315 summary: self.summary,
316 semantic_off: self.semantic_off,
317 verbosity: self.verbosity,
318 verbose_macro_warnings: self.verbose_macro_warnings,
319 show_macro_stats: self.show_macro_stats,
320 min_priority: self.min_priority,
321 min_score: self.min_score,
322 filter_categories: self.filter_categories,
323 no_context_aware: self.no_context_aware,
324 threshold_preset: self.threshold_preset,
325 _formatting_config: self._formatting_config,
326 parallel: self.parallel,
327 jobs: self.jobs,
328 multi_pass: self.multi_pass,
329 show_attribution: self.show_attribution,
330 aggregate_only: self.aggregate_only,
331 no_aggregation: self.no_aggregation,
332 aggregation_method: self.aggregation_method,
333 min_problematic: self.min_problematic,
334 no_god_object: self.no_god_object,
335 max_files: self.max_files,
336 debug_call_graph: self.debug_call_graph,
337 trace_functions: self.trace_functions,
338 call_graph_stats_only: self.call_graph_stats_only,
339 debug_format: self.debug_format,
340 validate_call_graph: self.validate_call_graph,
341 ast_functional_analysis: self.ast_functional_analysis,
342 functional_analysis_profile: self.functional_analysis_profile,
343 no_tui: self.no_tui,
344 show_filter_stats: self.show_filter_stats,
345 reference_time: self.reference_time,
346 };
347
348 super::analyze::handle_analyze(old_config).context("Analysis execution failed")
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 fn test_config(
357 path: PathBuf,
358 threshold_complexity: u32,
359 min_priority: Option<String>,
360 min_score: Option<f64>,
361 ) -> AnalyzeConfig<Unvalidated> {
362 AnalyzeConfig::new(
363 path,
364 cli::OutputFormat::Terminal,
365 None,
366 threshold_complexity,
367 5,
368 None,
369 None,
370 false,
371 None,
372 None,
373 None,
374 None,
375 false,
376 false,
377 0,
378 false,
379 false,
380 min_priority,
381 min_score,
382 None,
383 false,
384 None,
385 FormattingConfig::default(),
386 true,
387 4,
388 true,
389 false,
390 false,
391 false,
392 None,
393 None,
394 false,
395 None,
396 false,
397 None,
398 false,
399 cli::DebugFormatArg::Text,
400 false,
401 false,
402 None,
403 false,
404 false,
405 chrono::Utc::now(),
406 )
407 }
408
409 #[test]
410 fn test_validation_transition() {
411 let config = test_config(PathBuf::from("."), 10, None, None);
412
413 let validated = config.validate();
415 assert!(validated.is_ok());
416 }
417
418 #[test]
419 fn test_validation_fails_for_nonexistent_path() {
420 let config = test_config(
421 PathBuf::from("/nonexistent/path/that/should/not/exist"),
422 10,
423 None,
424 None,
425 );
426
427 let result = config.validate();
429 assert!(result.is_err());
430 }
431
432 #[test]
433 fn test_validation_fails_for_zero_complexity() {
434 let config = test_config(PathBuf::from("."), 0, None, None);
435
436 let result = config.validate();
437 assert!(result.is_err());
438 }
439
440 #[test]
441 fn test_validation_fails_for_invalid_score() {
442 let config = test_config(PathBuf::from("."), 10, None, Some(150.0));
443
444 let result = config.validate();
445 assert!(result.is_err());
446 }
447
448 #[test]
449 fn test_validation_fails_for_invalid_min_priority() {
450 let config = test_config(PathBuf::from("."), 10, Some("urgent".to_string()), None);
451
452 let result = config.validate();
453 assert!(result.is_err());
454 }
455}