1use std::collections::HashMap;
46use std::path::{Path, PathBuf};
47
48use rayon::prelude::*;
49use serde::{Deserialize, Serialize};
50use tracing::debug;
51use tree_sitter::{Node, Tree};
52
53use crate::ast::AstExtractor;
54use crate::callgraph::scanner::{ProjectScanner, ScanConfig};
55use crate::error::{Result, BrrrError};
56use crate::lang::LanguageRegistry;
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum NestingDepthLevel {
69 Good,
71 Acceptable,
73 Complex,
75 Severe,
77}
78
79impl NestingDepthLevel {
80 #[must_use]
82 pub fn from_depth(depth: u32) -> Self {
83 match depth {
84 0..=3 => Self::Good,
85 4..=5 => Self::Acceptable,
86 6..=7 => Self::Complex,
87 _ => Self::Severe,
88 }
89 }
90
91 #[must_use]
93 pub const fn description(&self) -> &'static str {
94 match self {
95 Self::Good => "Good nesting depth, code is readable",
96 Self::Acceptable => "Acceptable, consider simplifying if possible",
97 Self::Complex => "Complex nesting, hard to understand",
98 Self::Severe => "Severe nesting, refactor immediately",
99 }
100 }
101
102 #[must_use]
104 pub const fn color_code(&self) -> &'static str {
105 match self {
106 Self::Good => "\x1b[32m", Self::Acceptable => "\x1b[33m", Self::Complex => "\x1b[31m", Self::Severe => "\x1b[35m", }
111 }
112
113 #[must_use]
115 pub const fn threshold(&self) -> u32 {
116 match self {
117 Self::Good => 3,
118 Self::Acceptable => 5,
119 Self::Complex => 7,
120 Self::Severe => u32::MAX,
121 }
122 }
123}
124
125impl std::fmt::Display for NestingDepthLevel {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 match self {
128 Self::Good => write!(f, "good"),
129 Self::Acceptable => write!(f, "acceptable"),
130 Self::Complex => write!(f, "complex"),
131 Self::Severe => write!(f, "severe"),
132 }
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(rename_all = "snake_case")]
139pub enum NestingConstruct {
140 If,
142 ElseIf,
144 Else,
146 For,
148 While,
150 DoWhile,
152 Switch,
154 Try,
156 Catch,
158 Finally,
160 Lambda,
162 Closure,
164 Callback,
166 Comprehension,
168 With,
170 Unsafe,
172 Async,
174 Loop,
176 Select,
178 Defer,
180 Block,
182 Other(String),
184}
185
186impl std::fmt::Display for NestingConstruct {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 Self::If => write!(f, "if"),
190 Self::ElseIf => write!(f, "else if"),
191 Self::Else => write!(f, "else"),
192 Self::For => write!(f, "for"),
193 Self::While => write!(f, "while"),
194 Self::DoWhile => write!(f, "do-while"),
195 Self::Switch => write!(f, "switch/match"),
196 Self::Try => write!(f, "try"),
197 Self::Catch => write!(f, "catch"),
198 Self::Finally => write!(f, "finally"),
199 Self::Lambda => write!(f, "lambda"),
200 Self::Closure => write!(f, "closure"),
201 Self::Callback => write!(f, "callback"),
202 Self::Comprehension => write!(f, "comprehension"),
203 Self::With => write!(f, "with"),
204 Self::Unsafe => write!(f, "unsafe"),
205 Self::Async => write!(f, "async"),
206 Self::Loop => write!(f, "loop"),
207 Self::Select => write!(f, "select"),
208 Self::Defer => write!(f, "defer"),
209 Self::Block => write!(f, "block"),
210 Self::Other(name) => write!(f, "{}", name),
211 }
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct DeepNesting {
218 pub line: usize,
220 pub depth: u32,
222 pub construct_stack: Vec<String>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct NestingMetrics {
229 pub function_name: String,
231 pub file: PathBuf,
233 pub line: usize,
235 pub end_line: usize,
237 pub max_depth: u32,
239 pub average_depth: f64,
241 pub deepest_line: usize,
243 pub deep_locations: Vec<DeepNesting>,
245 pub risk_level: NestingDepthLevel,
247 pub suggestions: Vec<String>,
249 pub depth_distribution: HashMap<u32, usize>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct FunctionNesting {
256 pub name: String,
258 pub line: usize,
260 pub max_depth: u32,
262 pub risk_level: NestingDepthLevel,
264}
265
266impl From<&NestingMetrics> for FunctionNesting {
267 fn from(nm: &NestingMetrics) -> Self {
268 Self {
269 name: nm.function_name.clone(),
270 line: nm.line,
271 max_depth: nm.max_depth,
272 risk_level: nm.risk_level,
273 }
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct NestingStats {
280 pub total_functions: usize,
282 pub average_max_depth: f64,
284 pub global_max_depth: u32,
286 pub min_max_depth: u32,
288 pub median_max_depth: u32,
290 pub risk_distribution: HashMap<String, usize>,
292 pub functions_over_threshold: usize,
294}
295
296impl NestingStats {
297 fn from_metrics(results: &[NestingMetrics]) -> Self {
299 if results.is_empty() {
300 return Self {
301 total_functions: 0,
302 average_max_depth: 0.0,
303 global_max_depth: 0,
304 min_max_depth: 0,
305 median_max_depth: 0,
306 risk_distribution: HashMap::new(),
307 functions_over_threshold: 0,
308 };
309 }
310
311 let depths: Vec<u32> = results.iter().map(|r| r.max_depth).collect();
312 let total = depths.len();
313 let sum: u64 = depths.iter().map(|&d| u64::from(d)).sum();
314 let average = sum as f64 / total as f64;
315
316 let max = depths.iter().copied().max().unwrap_or(0);
317 let min = depths.iter().copied().min().unwrap_or(0);
318
319 let mut sorted = depths.clone();
320 sorted.sort_unstable();
321 let median = if total % 2 == 0 {
322 (sorted[total / 2 - 1] + sorted[total / 2]) / 2
323 } else {
324 sorted[total / 2]
325 };
326
327 let mut risk_distribution = HashMap::new();
328 for r in results {
329 *risk_distribution
330 .entry(r.risk_level.to_string())
331 .or_insert(0) += 1;
332 }
333
334 let functions_over_threshold = results.iter().filter(|r| r.max_depth > 5).count();
335
336 Self {
337 total_functions: total,
338 average_max_depth: average,
339 global_max_depth: max,
340 min_max_depth: min,
341 median_max_depth: median,
342 risk_distribution,
343 functions_over_threshold,
344 }
345 }
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct NestingAnalysis {
351 pub path: PathBuf,
353 pub language: Option<String>,
355 pub functions: Vec<NestingMetrics>,
357 #[serde(skip_serializing_if = "Option::is_none")]
359 pub violations: Option<Vec<NestingMetrics>>,
360 pub stats: NestingStats,
362 #[serde(skip_serializing_if = "Option::is_none")]
364 pub threshold: Option<u32>,
365 #[serde(skip_serializing_if = "Vec::is_empty")]
367 pub errors: Vec<NestingAnalysisError>,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct NestingAnalysisError {
373 pub file: PathBuf,
375 pub message: String,
377}
378
379struct NestingCalculator<'a> {
385 language: &'a str,
386 current_depth: u32,
387 max_depth: u32,
388 deepest_line: usize,
389 construct_stack: Vec<NestingConstruct>,
391 line_depths: HashMap<usize, u32>,
393 deep_locations: Vec<DeepNesting>,
395 threshold: u32,
397}
398
399impl<'a> NestingCalculator<'a> {
400 fn new(language: &'a str, threshold: u32) -> Self {
401 Self {
402 language,
403 current_depth: 0,
404 max_depth: 0,
405 deepest_line: 0,
406 construct_stack: Vec::new(),
407 line_depths: HashMap::new(),
408 deep_locations: Vec::new(),
409 threshold,
410 }
411 }
412
413 fn enter_nesting(&mut self, construct: NestingConstruct, line: usize) {
415 self.current_depth += 1;
416 self.construct_stack.push(construct);
417
418 if self.current_depth > self.max_depth {
420 self.max_depth = self.current_depth;
421 self.deepest_line = line;
422 }
423
424 self.line_depths
426 .entry(line)
427 .and_modify(|d| *d = (*d).max(self.current_depth))
428 .or_insert(self.current_depth);
429
430 if self.current_depth > self.threshold {
432 let stack: Vec<String> = self
433 .construct_stack
434 .iter()
435 .map(|c| c.to_string())
436 .collect();
437
438 self.deep_locations.push(DeepNesting {
439 line,
440 depth: self.current_depth,
441 construct_stack: stack,
442 });
443 }
444 }
445
446 fn exit_nesting(&mut self) {
448 self.current_depth = self.current_depth.saturating_sub(1);
449 self.construct_stack.pop();
450 }
451
452 fn process_node(&mut self, node: Node) {
454 let line = node.start_position().row + 1;
455 let kind = node.kind();
456
457 self.line_depths
459 .entry(line)
460 .and_modify(|d| *d = (*d).max(self.current_depth))
461 .or_insert(self.current_depth);
462
463 let construct = self.classify_nesting_construct(node, kind);
465
466 if let Some(c) = construct {
467 self.enter_nesting(c, line);
468 self.process_children(node);
469 self.exit_nesting();
470 } else {
471 self.process_children(node);
472 }
473 }
474
475 fn process_children(&mut self, node: Node) {
477 let mut cursor = node.walk();
478 for child in node.children(&mut cursor) {
479 self.process_node(child);
480 }
481 }
482
483 fn classify_nesting_construct(&self, node: Node, kind: &str) -> Option<NestingConstruct> {
485 match self.language {
486 "python" => self.classify_python_construct(node, kind),
487 "typescript" | "javascript" | "tsx" | "jsx" => {
488 self.classify_javascript_construct(node, kind)
489 }
490 "rust" => self.classify_rust_construct(node, kind),
491 "go" => self.classify_go_construct(node, kind),
492 "java" => self.classify_java_construct(node, kind),
493 "c" | "cpp" => self.classify_c_construct(node, kind),
494 _ => self.classify_generic_construct(kind),
495 }
496 }
497
498 fn classify_python_construct(&self, node: Node, kind: &str) -> Option<NestingConstruct> {
500 match kind {
501 "if_statement" => Some(NestingConstruct::If),
502 "elif_clause" => Some(NestingConstruct::ElseIf),
503 "else_clause" => {
504 if node.parent().is_some_and(|p| p.kind() == "if_statement") {
506 Some(NestingConstruct::Else)
507 } else {
508 None
509 }
510 }
511 "for_statement" => Some(NestingConstruct::For),
512 "while_statement" => Some(NestingConstruct::While),
513 "try_statement" => Some(NestingConstruct::Try),
514 "except_clause" | "except_group_clause" => Some(NestingConstruct::Catch),
515 "finally_clause" => Some(NestingConstruct::Finally),
516 "with_statement" => Some(NestingConstruct::With),
517 "match_statement" => Some(NestingConstruct::Switch),
518 "lambda" => Some(NestingConstruct::Lambda),
519 "list_comprehension" | "dictionary_comprehension" | "set_comprehension"
520 | "generator_expression" => Some(NestingConstruct::Comprehension),
521 _ => None,
522 }
523 }
524
525 fn classify_javascript_construct(&self, node: Node, kind: &str) -> Option<NestingConstruct> {
527 match kind {
528 "if_statement" => Some(NestingConstruct::If),
529 "else_clause" => Some(NestingConstruct::Else),
530 "for_statement" | "for_in_statement" | "for_of_statement" => {
531 Some(NestingConstruct::For)
532 }
533 "while_statement" => Some(NestingConstruct::While),
534 "do_statement" => Some(NestingConstruct::DoWhile),
535 "switch_statement" => Some(NestingConstruct::Switch),
536 "try_statement" => Some(NestingConstruct::Try),
537 "catch_clause" => Some(NestingConstruct::Catch),
538 "finally_clause" => Some(NestingConstruct::Finally),
539 "arrow_function" | "function_expression" => {
540 if self.is_callback(node) {
542 Some(NestingConstruct::Callback)
543 } else {
544 Some(NestingConstruct::Lambda)
545 }
546 }
547 _ => None,
548 }
549 }
550
551 fn is_callback(&self, node: Node) -> bool {
553 let mut current = node.parent();
554 while let Some(parent) = current {
555 match parent.kind() {
556 "call_expression" | "method_invocation" => return true,
557 "arguments" | "argument_list" => {
558 }
560 "function_declaration"
561 | "function_definition"
562 | "method_definition"
563 | "arrow_function" => {
564 return false;
566 }
567 _ => {}
568 }
569 current = parent.parent();
570 }
571 false
572 }
573
574 fn classify_rust_construct(&self, _node: Node, kind: &str) -> Option<NestingConstruct> {
576 match kind {
577 "if_expression" | "if_let_expression" => Some(NestingConstruct::If),
578 "else_clause" => Some(NestingConstruct::Else),
579 "for_expression" => Some(NestingConstruct::For),
580 "while_expression" | "while_let_expression" => Some(NestingConstruct::While),
581 "loop_expression" => Some(NestingConstruct::Loop),
582 "match_expression" => Some(NestingConstruct::Switch),
583 "closure_expression" => Some(NestingConstruct::Closure),
584 "unsafe_block" => Some(NestingConstruct::Unsafe),
585 "async_block" => Some(NestingConstruct::Async),
586 "block" => {
587 None
590 }
591 _ => None,
592 }
593 }
594
595 fn classify_go_construct(&self, _node: Node, kind: &str) -> Option<NestingConstruct> {
597 match kind {
598 "if_statement" => Some(NestingConstruct::If),
599 "for_statement" => Some(NestingConstruct::For),
600 "expression_switch_statement" | "type_switch_statement" => {
601 Some(NestingConstruct::Switch)
602 }
603 "select_statement" => Some(NestingConstruct::Select),
604 "func_literal" => Some(NestingConstruct::Closure),
605 "defer_statement" => Some(NestingConstruct::Defer),
606 _ => None,
607 }
608 }
609
610 fn classify_java_construct(&self, _node: Node, kind: &str) -> Option<NestingConstruct> {
612 match kind {
613 "if_statement" => Some(NestingConstruct::If),
614 "for_statement" | "enhanced_for_statement" => Some(NestingConstruct::For),
615 "while_statement" => Some(NestingConstruct::While),
616 "do_statement" => Some(NestingConstruct::DoWhile),
617 "switch_expression" | "switch_statement" => Some(NestingConstruct::Switch),
618 "try_statement" => Some(NestingConstruct::Try),
619 "catch_clause" => Some(NestingConstruct::Catch),
620 "finally_clause" => Some(NestingConstruct::Finally),
621 "lambda_expression" => Some(NestingConstruct::Lambda),
622 _ => None,
623 }
624 }
625
626 fn classify_c_construct(&self, _node: Node, kind: &str) -> Option<NestingConstruct> {
628 match kind {
629 "if_statement" => Some(NestingConstruct::If),
630 "else_clause" => Some(NestingConstruct::Else),
631 "for_statement" | "for_range_loop" => Some(NestingConstruct::For),
632 "while_statement" => Some(NestingConstruct::While),
633 "do_statement" => Some(NestingConstruct::DoWhile),
634 "switch_statement" => Some(NestingConstruct::Switch),
635 "try_statement" => Some(NestingConstruct::Try),
636 "catch_clause" => Some(NestingConstruct::Catch),
637 "lambda_expression" => Some(NestingConstruct::Lambda),
638 _ => None,
639 }
640 }
641
642 fn classify_generic_construct(&self, kind: &str) -> Option<NestingConstruct> {
644 if kind.contains("if") && !kind.contains("elif") {
645 Some(NestingConstruct::If)
646 } else if kind.contains("for") || kind.contains("while") {
647 Some(NestingConstruct::For)
648 } else if kind.contains("switch") || kind.contains("match") {
649 Some(NestingConstruct::Switch)
650 } else if kind.contains("try") {
651 Some(NestingConstruct::Try)
652 } else if kind.contains("catch") || kind.contains("except") {
653 Some(NestingConstruct::Catch)
654 } else if kind.contains("lambda") || kind.contains("closure") {
655 Some(NestingConstruct::Lambda)
656 } else {
657 None
658 }
659 }
660
661 fn average_depth(&self) -> f64 {
663 if self.line_depths.is_empty() {
664 return 0.0;
665 }
666 let sum: u64 = self.line_depths.values().map(|&d| u64::from(d)).sum();
667 sum as f64 / self.line_depths.len() as f64
668 }
669
670 fn depth_distribution(&self) -> HashMap<u32, usize> {
672 let mut dist = HashMap::new();
673 for &depth in self.line_depths.values() {
674 *dist.entry(depth).or_insert(0) += 1;
675 }
676 dist
677 }
678}
679
680fn generate_suggestions(metrics: &NestingMetrics, language: &str) -> Vec<String> {
686 let mut suggestions = Vec::new();
687
688 if metrics.max_depth <= 3 {
689 return suggestions;
690 }
691
692 for location in &metrics.deep_locations {
694 let stack = &location.construct_stack;
695
696 if stack.first() == Some(&"if".to_string())
698 && stack.iter().filter(|&s| s == "if").count() >= 2
699 {
700 suggestions.push(format!(
701 "Consider using early return/guard clauses to reduce nesting at line {}",
702 location.line
703 ));
704 }
705
706 if location.depth >= 5 {
708 suggestions.push(format!(
709 "Consider extracting nested code at line {} into a separate function",
710 location.line
711 ));
712 }
713
714 if (language == "javascript" || language == "typescript")
716 && stack.iter().filter(|s| *s == "callback").count() >= 2
717 {
718 suggestions.push(format!(
719 "Consider using async/await or Promises to flatten callbacks at line {}",
720 location.line
721 ));
722 }
723
724 if stack.contains(&"for".to_string()) && stack.contains(&"if".to_string()) {
726 suggestions.push(format!(
727 "Consider using filter/map/reduce to simplify loop with conditions at line {}",
728 location.line
729 ));
730 }
731 }
732
733 if metrics.max_depth >= 6 {
735 suggestions.push(
736 "Consider applying the 'Extract Method' refactoring to reduce overall complexity"
737 .to_string(),
738 );
739 }
740
741 if metrics.max_depth >= 4 && metrics.average_depth > 2.0 {
742 suggestions.push(
743 "Consider restructuring to use polymorphism instead of nested conditionals".to_string(),
744 );
745 }
746
747 suggestions.sort();
749 suggestions.dedup();
750
751 suggestions
752}
753
754pub fn analyze_nesting(
770 path: impl AsRef<Path>,
771 language: Option<&str>,
772 threshold: Option<u32>,
773) -> Result<NestingAnalysis> {
774 let path = path.as_ref();
775
776 if !path.exists() {
777 return Err(BrrrError::Io(std::io::Error::new(
778 std::io::ErrorKind::NotFound,
779 format!("Path not found: {}", path.display()),
780 )));
781 }
782
783 if path.is_file() {
784 return analyze_file_nesting(path, threshold);
785 }
786
787 let path_str = path
789 .to_str()
790 .ok_or_else(|| BrrrError::InvalidArgument("Invalid path encoding".to_string()))?;
791
792 let scanner = ProjectScanner::new(path_str)?;
793
794 let config = if let Some(lang) = language {
795 ScanConfig::for_language(lang)
796 } else {
797 ScanConfig::default()
798 };
799
800 let scan_result = scanner.scan_with_config(&config)?;
801
802 if scan_result.files.is_empty() {
803 return Err(BrrrError::InvalidArgument(format!(
804 "No source files found in {} (filter: {:?})",
805 path.display(),
806 language
807 )));
808 }
809
810 debug!(
811 "Analyzing {} files for nesting depth",
812 scan_result.files.len()
813 );
814
815 let depth_threshold = threshold.unwrap_or(5);
816
817 let results: Vec<(Vec<NestingMetrics>, Vec<NestingAnalysisError>)> = scan_result
819 .files
820 .par_iter()
821 .map(|file| analyze_file_functions_nesting(file, depth_threshold))
822 .collect();
823
824 let mut all_functions = Vec::new();
826 let mut all_errors = Vec::new();
827
828 for (functions, errors) in results {
829 all_functions.extend(functions);
830 all_errors.extend(errors);
831 }
832
833 let stats = NestingStats::from_metrics(&all_functions);
835
836 let violations = threshold.map(|t| {
838 all_functions
839 .iter()
840 .filter(|f| f.max_depth > t)
841 .cloned()
842 .collect::<Vec<_>>()
843 });
844
845 Ok(NestingAnalysis {
846 path: path.to_path_buf(),
847 language: language.map(String::from),
848 functions: all_functions,
849 violations,
850 stats,
851 threshold,
852 errors: all_errors,
853 })
854}
855
856pub fn analyze_file_nesting(
858 file: impl AsRef<Path>,
859 threshold: Option<u32>,
860) -> Result<NestingAnalysis> {
861 let file = file.as_ref();
862
863 if !file.exists() {
864 return Err(BrrrError::Io(std::io::Error::new(
865 std::io::ErrorKind::NotFound,
866 format!("File not found: {}", file.display()),
867 )));
868 }
869
870 if !file.is_file() {
871 return Err(BrrrError::InvalidArgument(format!(
872 "Expected a file, got directory: {}",
873 file.display()
874 )));
875 }
876
877 let depth_threshold = threshold.unwrap_or(5);
878 let (functions, errors) = analyze_file_functions_nesting(file, depth_threshold);
879
880 let stats = NestingStats::from_metrics(&functions);
881
882 let violations = threshold.map(|t| {
883 functions
884 .iter()
885 .filter(|f| f.max_depth > t)
886 .cloned()
887 .collect::<Vec<_>>()
888 });
889
890 let registry = LanguageRegistry::global();
891 let language = registry
892 .detect_language(file)
893 .map(|l| l.name().to_string());
894
895 Ok(NestingAnalysis {
896 path: file.to_path_buf(),
897 language,
898 functions,
899 violations,
900 stats,
901 threshold,
902 errors,
903 })
904}
905
906fn analyze_file_functions_nesting(
908 file: &Path,
909 threshold: u32,
910) -> (Vec<NestingMetrics>, Vec<NestingAnalysisError>) {
911 let mut results = Vec::new();
912 let mut errors = Vec::new();
913
914 let source = match std::fs::read(file) {
916 Ok(s) => s,
917 Err(e) => {
918 errors.push(NestingAnalysisError {
919 file: file.to_path_buf(),
920 message: format!("Failed to read file: {}", e),
921 });
922 return (results, errors);
923 }
924 };
925
926 let registry = LanguageRegistry::global();
928 let lang = match registry.detect_language(file) {
929 Some(l) => l,
930 None => {
931 errors.push(NestingAnalysisError {
932 file: file.to_path_buf(),
933 message: "Unknown language".to_string(),
934 });
935 return (results, errors);
936 }
937 };
938
939 let mut parser = match lang.parser() {
941 Ok(p) => p,
942 Err(e) => {
943 errors.push(NestingAnalysisError {
944 file: file.to_path_buf(),
945 message: format!("Failed to create parser: {}", e),
946 });
947 return (results, errors);
948 }
949 };
950
951 let tree = match parser.parse(&source, None) {
952 Some(t) => t,
953 None => {
954 errors.push(NestingAnalysisError {
955 file: file.to_path_buf(),
956 message: "Failed to parse file".to_string(),
957 });
958 return (results, errors);
959 }
960 };
961
962 let module = match AstExtractor::extract_file(file) {
964 Ok(m) => m,
965 Err(e) => {
966 errors.push(NestingAnalysisError {
967 file: file.to_path_buf(),
968 message: format!("Failed to extract AST: {}", e),
969 });
970 return (results, errors);
971 }
972 };
973
974 let lang_name = lang.name();
975
976 for func in &module.functions {
978 let result = analyze_function_nesting(
979 &source,
980 &tree,
981 &func.name,
982 func.line_number,
983 func.end_line_number.unwrap_or(func.line_number),
984 file,
985 lang_name,
986 threshold,
987 );
988 results.push(result);
989 }
990
991 for class in &module.classes {
993 for method in &class.methods {
994 let qualified_name = format!("{}.{}", class.name, method.name);
995 let mut result = analyze_function_nesting(
996 &source,
997 &tree,
998 &qualified_name,
999 method.line_number,
1000 method.end_line_number.unwrap_or(method.line_number),
1001 file,
1002 lang_name,
1003 threshold,
1004 );
1005 result.function_name = qualified_name;
1006 results.push(result);
1007 }
1008
1009 analyze_nested_classes_nesting(
1011 &source,
1012 &tree,
1013 class,
1014 &class.name,
1015 file,
1016 lang_name,
1017 threshold,
1018 &mut results,
1019 );
1020 }
1021
1022 (results, errors)
1023}
1024
1025fn analyze_nested_classes_nesting(
1027 source: &[u8],
1028 tree: &Tree,
1029 class: &crate::ast::types::ClassInfo,
1030 class_prefix: &str,
1031 file: &Path,
1032 lang_name: &str,
1033 threshold: u32,
1034 results: &mut Vec<NestingMetrics>,
1035) {
1036 for nested in &class.inner_classes {
1037 let nested_prefix = format!("{}.{}", class_prefix, nested.name);
1038
1039 for method in &nested.methods {
1040 let qualified_name = format!("{}.{}", nested_prefix, method.name);
1041 let mut result = analyze_function_nesting(
1042 source,
1043 tree,
1044 &qualified_name,
1045 method.line_number,
1046 method.end_line_number.unwrap_or(method.line_number),
1047 file,
1048 lang_name,
1049 threshold,
1050 );
1051 result.function_name = qualified_name;
1052 results.push(result);
1053 }
1054
1055 analyze_nested_classes_nesting(
1057 source,
1058 tree,
1059 nested,
1060 &nested_prefix,
1061 file,
1062 lang_name,
1063 threshold,
1064 results,
1065 );
1066 }
1067}
1068
1069fn analyze_function_nesting(
1071 source: &[u8],
1072 tree: &Tree,
1073 function_name: &str,
1074 start_line: usize,
1075 end_line: usize,
1076 file: &Path,
1077 language: &str,
1078 threshold: u32,
1079) -> NestingMetrics {
1080 let func_node = find_function_node(tree.root_node(), function_name, start_line, source, language);
1082
1083 if let Some(node) = func_node {
1084 let mut calculator = NestingCalculator::new(language, threshold);
1085
1086 if let Some(body) = find_function_body(node, language) {
1088 calculator.process_node(body);
1089 } else {
1090 let mut cursor = node.walk();
1092 for child in node.children(&mut cursor) {
1093 if !is_function_signature_part(child.kind(), language) {
1094 calculator.process_node(child);
1095 }
1096 }
1097 }
1098
1099 let average = calculator.average_depth();
1100 let distribution = calculator.depth_distribution();
1101 let risk_level = NestingDepthLevel::from_depth(calculator.max_depth);
1102
1103 let mut metrics = NestingMetrics {
1104 function_name: function_name.to_string(),
1105 file: file.to_path_buf(),
1106 line: start_line,
1107 end_line,
1108 max_depth: calculator.max_depth,
1109 average_depth: average,
1110 deepest_line: calculator.deepest_line,
1111 deep_locations: calculator.deep_locations,
1112 risk_level,
1113 suggestions: Vec::new(),
1114 depth_distribution: distribution,
1115 };
1116
1117 metrics.suggestions = generate_suggestions(&metrics, language);
1119
1120 return metrics;
1121 }
1122
1123 NestingMetrics {
1125 function_name: function_name.to_string(),
1126 file: file.to_path_buf(),
1127 line: start_line,
1128 end_line,
1129 max_depth: 0,
1130 average_depth: 0.0,
1131 deepest_line: start_line,
1132 deep_locations: Vec::new(),
1133 risk_level: NestingDepthLevel::Good,
1134 suggestions: Vec::new(),
1135 depth_distribution: HashMap::new(),
1136 }
1137}
1138
1139fn find_function_body<'a>(node: Node<'a>, language: &str) -> Option<Node<'a>> {
1141 let body_field = match language {
1142 "python" => "body",
1143 "typescript" | "javascript" | "tsx" | "jsx" => "body",
1144 "rust" => "body",
1145 "go" => "body",
1146 "java" => "body",
1147 "c" | "cpp" => "body",
1148 _ => "body",
1149 };
1150
1151 node.child_by_field_name(body_field)
1152}
1153
1154fn is_function_signature_part(kind: &str, _language: &str) -> bool {
1156 matches!(
1157 kind,
1158 "parameters"
1159 | "formal_parameters"
1160 | "parameter_list"
1161 | "type_parameters"
1162 | "return_type"
1163 | "type_annotation"
1164 | "type"
1165 | "decorator"
1166 | "modifiers"
1167 | "visibility_modifier"
1168 | "identifier"
1169 | "name"
1170 | "def"
1171 | "fn"
1172 | "func"
1173 | "function"
1174 | "async"
1175 | "("
1176 | ")"
1177 | "{"
1178 | "}"
1179 | ":"
1180 | "->"
1181 )
1182}
1183
1184fn find_function_node<'a>(
1186 root: Node<'a>,
1187 function_name: &str,
1188 start_line: usize,
1189 source: &[u8],
1190 language: &str,
1191) -> Option<Node<'a>> {
1192 let simple_name = function_name.rsplit('.').next().unwrap_or(function_name);
1193
1194 let function_kinds: &[&str] = match language {
1195 "python" => &["function_definition"],
1196 "typescript" | "javascript" | "tsx" | "jsx" => {
1197 &["function_declaration", "method_definition", "arrow_function"]
1198 }
1199 "rust" => &["function_item"],
1200 "go" => &["function_declaration", "method_declaration"],
1201 "java" => &["method_declaration", "constructor_declaration"],
1202 "c" | "cpp" => &["function_definition"],
1203 _ => &["function_definition", "function_declaration"],
1204 };
1205
1206 find_node_recursive(root, simple_name, start_line, source, function_kinds)
1207}
1208
1209fn find_node_recursive<'a>(
1211 node: Node<'a>,
1212 target_name: &str,
1213 target_line: usize,
1214 source: &[u8],
1215 function_kinds: &[&str],
1216) -> Option<Node<'a>> {
1217 let node_line = node.start_position().row + 1;
1218
1219 if function_kinds.contains(&node.kind()) && node_line == target_line {
1220 if let Some(name_node) = node.child_by_field_name("name") {
1221 let name =
1222 std::str::from_utf8(&source[name_node.start_byte()..name_node.end_byte()])
1223 .unwrap_or("");
1224 if name == target_name {
1225 return Some(node);
1226 }
1227 }
1228 }
1229
1230 let mut cursor = node.walk();
1231 for child in node.children(&mut cursor) {
1232 if let Some(found) =
1233 find_node_recursive(child, target_name, target_line, source, function_kinds)
1234 {
1235 return Some(found);
1236 }
1237 }
1238
1239 None
1240}
1241
1242#[cfg(test)]
1247mod tests {
1248 use super::*;
1249 use std::io::Write;
1250 use tempfile::NamedTempFile;
1251
1252 fn create_temp_file(content: &str, extension: &str) -> NamedTempFile {
1253 let mut file = tempfile::Builder::new()
1254 .suffix(extension)
1255 .tempfile()
1256 .expect("Failed to create temp file");
1257 file.write_all(content.as_bytes())
1258 .expect("Failed to write to temp file");
1259 file
1260 }
1261
1262 #[test]
1263 fn test_depth_level_classification() {
1264 assert_eq!(NestingDepthLevel::from_depth(0), NestingDepthLevel::Good);
1265 assert_eq!(NestingDepthLevel::from_depth(3), NestingDepthLevel::Good);
1266 assert_eq!(NestingDepthLevel::from_depth(4), NestingDepthLevel::Acceptable);
1267 assert_eq!(NestingDepthLevel::from_depth(5), NestingDepthLevel::Acceptable);
1268 assert_eq!(NestingDepthLevel::from_depth(6), NestingDepthLevel::Complex);
1269 assert_eq!(NestingDepthLevel::from_depth(7), NestingDepthLevel::Complex);
1270 assert_eq!(NestingDepthLevel::from_depth(8), NestingDepthLevel::Severe);
1271 assert_eq!(NestingDepthLevel::from_depth(100), NestingDepthLevel::Severe);
1272 }
1273
1274 #[test]
1275 fn test_simple_function_no_nesting() {
1276 let source = r#"
1277def simple():
1278 return 42
1279"#;
1280 let file = create_temp_file(source, ".py");
1281 let result = analyze_file_nesting(file.path(), None);
1282
1283 assert!(result.is_ok());
1284 let analysis = result.unwrap();
1285
1286 assert_eq!(analysis.functions.len(), 1);
1287 assert_eq!(analysis.functions[0].function_name, "simple");
1288 assert_eq!(analysis.functions[0].max_depth, 0);
1289 assert_eq!(analysis.functions[0].risk_level, NestingDepthLevel::Good);
1290 }
1291
1292 #[test]
1293 fn test_single_if_nesting() {
1294 let source = r#"
1295def with_if(x):
1296 if x > 0:
1297 return 1
1298 return 0
1299"#;
1300 let file = create_temp_file(source, ".py");
1301 let result = analyze_file_nesting(file.path(), None);
1302
1303 assert!(result.is_ok());
1304 let analysis = result.unwrap();
1305
1306 assert_eq!(analysis.functions.len(), 1);
1307 assert_eq!(analysis.functions[0].max_depth, 1);
1308 }
1309
1310 #[test]
1311 fn test_nested_constructs() {
1312 let source = r#"
1313def nested(x, items):
1314 if x > 0:
1315 for item in items:
1316 if item > 5:
1317 print(item)
1318"#;
1319 let file = create_temp_file(source, ".py");
1320 let result = analyze_file_nesting(file.path(), None);
1321
1322 assert!(result.is_ok());
1323 let analysis = result.unwrap();
1324
1325 assert_eq!(analysis.functions.len(), 1);
1326 assert_eq!(analysis.functions[0].max_depth, 3);
1328 assert_eq!(analysis.functions[0].risk_level, NestingDepthLevel::Good);
1329 }
1330
1331 #[test]
1332 fn test_deeply_nested() {
1333 let source = r#"
1334def deeply_nested(a, b, c, d, e, f):
1335 if a:
1336 if b:
1337 if c:
1338 if d:
1339 if e:
1340 if f:
1341 return True
1342 return False
1343"#;
1344 let file = create_temp_file(source, ".py");
1345 let result = analyze_file_nesting(file.path(), Some(3));
1346
1347 assert!(result.is_ok());
1348 let analysis = result.unwrap();
1349
1350 assert_eq!(analysis.functions.len(), 1);
1351 assert_eq!(analysis.functions[0].max_depth, 6);
1352 assert_eq!(analysis.functions[0].risk_level, NestingDepthLevel::Complex);
1353
1354 assert!(!analysis.functions[0].deep_locations.is_empty());
1356
1357 assert!(!analysis.functions[0].suggestions.is_empty());
1359 }
1360
1361 #[test]
1362 fn test_try_except_nesting() {
1363 let source = r#"
1364def with_try(x):
1365 try:
1366 if x > 0:
1367 return 1 / x
1368 except ZeroDivisionError:
1369 return 0
1370"#;
1371 let file = create_temp_file(source, ".py");
1372 let result = analyze_file_nesting(file.path(), None);
1373
1374 assert!(result.is_ok());
1375 let analysis = result.unwrap();
1376
1377 assert_eq!(analysis.functions.len(), 1);
1378 assert!(analysis.functions[0].max_depth >= 2);
1380 }
1381
1382 #[test]
1383 fn test_loop_nesting() {
1384 let source = r#"
1385def nested_loops(items):
1386 for i in items:
1387 for j in items:
1388 for k in items:
1389 print(i, j, k)
1390"#;
1391 let file = create_temp_file(source, ".py");
1392 let result = analyze_file_nesting(file.path(), None);
1393
1394 assert!(result.is_ok());
1395 let analysis = result.unwrap();
1396
1397 assert_eq!(analysis.functions.len(), 1);
1398 assert_eq!(analysis.functions[0].max_depth, 3);
1399 }
1400
1401 #[test]
1402 fn test_class_methods() {
1403 let source = r#"
1404class Example:
1405 def simple(self):
1406 return 1
1407
1408 def nested(self, x):
1409 if x > 0:
1410 for i in range(x):
1411 print(i)
1412"#;
1413 let file = create_temp_file(source, ".py");
1414 let result = analyze_file_nesting(file.path(), None);
1415
1416 assert!(result.is_ok());
1417 let analysis = result.unwrap();
1418
1419 assert_eq!(analysis.functions.len(), 2);
1420
1421 let simple = analysis
1422 .functions
1423 .iter()
1424 .find(|f| f.function_name == "Example.simple");
1425 let nested = analysis
1426 .functions
1427 .iter()
1428 .find(|f| f.function_name == "Example.nested");
1429
1430 assert!(simple.is_some());
1431 assert!(nested.is_some());
1432
1433 assert_eq!(simple.unwrap().max_depth, 0);
1434 assert_eq!(nested.unwrap().max_depth, 2);
1435 }
1436
1437 #[test]
1438 fn test_threshold_filtering() {
1439 let source = r#"
1440def shallow():
1441 if True:
1442 return 1
1443
1444def deep(a, b, c, d):
1445 if a:
1446 if b:
1447 if c:
1448 if d:
1449 return True
1450 return False
1451"#;
1452 let file = create_temp_file(source, ".py");
1453 let result = analyze_file_nesting(file.path(), Some(2));
1454
1455 assert!(result.is_ok());
1456 let analysis = result.unwrap();
1457
1458 assert!(analysis.violations.is_some());
1459 let violations = analysis.violations.unwrap();
1460
1461 assert_eq!(violations.len(), 1);
1463 assert_eq!(violations[0].function_name, "deep");
1464 }
1465
1466 #[test]
1467 fn test_typescript_nesting() {
1468 let source = r#"
1469function nested(x: number): void {
1470 if (x > 0) {
1471 for (let i = 0; i < x; i++) {
1472 console.log(i);
1473 }
1474 }
1475}
1476"#;
1477 let file = create_temp_file(source, ".ts");
1478 let result = analyze_file_nesting(file.path(), None);
1479
1480 assert!(result.is_ok());
1481 let analysis = result.unwrap();
1482
1483 assert_eq!(analysis.functions.len(), 1);
1484 assert_eq!(analysis.functions[0].max_depth, 2);
1485 }
1486
1487 #[test]
1488 fn test_statistics() {
1489 let metrics = vec![
1490 NestingMetrics {
1491 function_name: "f1".to_string(),
1492 file: PathBuf::new(),
1493 line: 1,
1494 end_line: 5,
1495 max_depth: 2,
1496 average_depth: 1.0,
1497 deepest_line: 3,
1498 deep_locations: vec![],
1499 risk_level: NestingDepthLevel::Good,
1500 suggestions: vec![],
1501 depth_distribution: HashMap::new(),
1502 },
1503 NestingMetrics {
1504 function_name: "f2".to_string(),
1505 file: PathBuf::new(),
1506 line: 10,
1507 end_line: 20,
1508 max_depth: 6,
1509 average_depth: 3.0,
1510 deepest_line: 15,
1511 deep_locations: vec![],
1512 risk_level: NestingDepthLevel::Complex,
1513 suggestions: vec![],
1514 depth_distribution: HashMap::new(),
1515 },
1516 ];
1517
1518 let stats = NestingStats::from_metrics(&metrics);
1519
1520 assert_eq!(stats.total_functions, 2);
1521 assert_eq!(stats.global_max_depth, 6);
1522 assert_eq!(stats.min_max_depth, 2);
1523 assert_eq!(stats.functions_over_threshold, 1);
1524 assert!((stats.average_max_depth - 4.0).abs() < 0.01);
1525 }
1526
1527 #[test]
1528 fn test_nonexistent_file() {
1529 let result = analyze_file_nesting("/nonexistent/path/file.py", None);
1530 assert!(result.is_err());
1531 }
1532
1533 #[test]
1534 fn test_empty_file() {
1535 let source = "# Just a comment\n";
1536 let file = create_temp_file(source, ".py");
1537 let result = analyze_file_nesting(file.path(), None);
1538
1539 assert!(result.is_ok());
1540 let analysis = result.unwrap();
1541
1542 assert_eq!(analysis.functions.len(), 0);
1543 assert_eq!(analysis.stats.total_functions, 0);
1544 }
1545
1546 #[test]
1547 fn test_comprehension_nesting() {
1548 let source = r#"
1549def with_comprehension(items):
1550 result = [x for x in items if x > 0]
1551 return result
1552"#;
1553 let file = create_temp_file(source, ".py");
1554 let result = analyze_file_nesting(file.path(), None);
1555
1556 assert!(result.is_ok());
1557 let analysis = result.unwrap();
1558
1559 assert_eq!(analysis.functions.len(), 1);
1560 assert!(analysis.functions[0].max_depth >= 1);
1562 }
1563
1564 #[test]
1565 fn test_lambda_nesting() {
1566 let source = r#"
1567def with_lambda(items):
1568 if items:
1569 result = map(lambda x: x * 2, items)
1570 return list(result)
1571 return []
1572"#;
1573 let file = create_temp_file(source, ".py");
1574 let result = analyze_file_nesting(file.path(), None);
1575
1576 assert!(result.is_ok());
1577 let analysis = result.unwrap();
1578
1579 assert_eq!(analysis.functions.len(), 1);
1580 assert!(analysis.functions[0].max_depth >= 2);
1582 }
1583
1584 #[test]
1585 fn test_with_statement_nesting() {
1586 let source = r#"
1587def with_context(path):
1588 with open(path) as f:
1589 for line in f:
1590 if line.strip():
1591 print(line)
1592"#;
1593 let file = create_temp_file(source, ".py");
1594 let result = analyze_file_nesting(file.path(), None);
1595
1596 assert!(result.is_ok());
1597 let analysis = result.unwrap();
1598
1599 assert_eq!(analysis.functions.len(), 1);
1600 assert_eq!(analysis.functions[0].max_depth, 3);
1602 }
1603}