diff --git a/codesnap/utils/complexity.py b/codesnap/utils/complexity.py new file mode 100644 index 0000000..077d962 --- /dev/null +++ b/codesnap/utils/complexity.py @@ -0,0 +1,173 @@ +"""Complexity calculation module for CodeSnap.""" + +from pathlib import Path +from typing import Optional + + +class ComplexityCalculator: + """Calculates cyclomatic complexity for source code.""" + + def __init__( + self, low_threshold: int = 10, medium_threshold: int = 20, high_threshold: int = 50 + ) -> None: + self.low_threshold = low_threshold + self.medium_threshold = medium_threshold + self.high_threshold = high_threshold + + def calculate(self, content: str, language: str = "python") -> int: + """Calculate cyclomatic complexity for a file.""" + complexity = 1 + + branching_keywords = self._get_branching_keywords(language) + for keyword in branching_keywords: + complexity += content.count(keyword) + + lines = content.split("\n") + max_nesting = self._calculate_max_nesting(lines, language) + complexity += max_nesting // 2 + + long_functions = self._count_long_functions(lines, language) + complexity += long_functions + + return complexity + + def _get_branching_keywords(self, language: str) -> list[str]: + """Get branching keywords for a language.""" + common_keywords = ["if ", "elif ", "else:", "for ", "while ", "except:", "finally:"] + language_keywords = { + "python": common_keywords + ["case ", "match "], + "javascript": common_keywords + + [ + "switch (", + "case ", + "try {", + "catch (", + "&&", + "||", + "??", + ], + "typescript": common_keywords + + [ + "switch (", + "case ", + "try {", + "catch (", + "&&", + "||", + "??", + ], + "go": ["if ", "else {", "for ", "switch ", "case ", "default:"], + "rust": ["if ", "else", "match ", "loop ", "while ", "match "], + } + return language_keywords.get(language, common_keywords) + + def _calculate_max_nesting(self, lines: list[str], language: str) -> int: + """Calculate maximum nesting depth in the code.""" + max_nesting = 0 + current_nesting = 0 + + indent_char = "\t" if language in ("python", "yaml") else " " + for line in lines: + stripped = line.strip() + if not stripped or stripped.startswith("#") or stripped.startswith("//"): + continue + + indent = len(line) - len(line.lstrip(indent_char)) + new_nesting = indent // (4 if indent_char == " " else 1) + + if new_nesting > current_nesting: + current_nesting = new_nesting + max_nesting = max(max_nesting, current_nesting) + + return max_nesting + + def _count_long_functions(self, lines: list[str], language: str) -> int: + """Count functions that exceed length threshold.""" + threshold = 50 + count = 0 + in_function = False + func_start = 0 + + for i, line in enumerate(lines): + if language == "python": + if line.strip().startswith("def "): + if in_function: + func_len = i - func_start + if func_len > threshold: + count += 1 + in_function = True + func_start = i + elif language in ("javascript", "typescript"): + if "function" in line or "=>" in line: + if in_function: + func_len = i - func_start + if func_len > threshold: + count += 1 + in_function = True + func_start = i + + if in_function: + func_len = len(lines) - func_start + if func_len > threshold: + count += 1 + + return count + + def get_rating(self, complexity: int) -> str: + """Get complexity rating string.""" + if complexity <= self.low_threshold: + return "low" + elif complexity <= self.medium_threshold: + return "medium" + elif complexity <= self.high_threshold: + return "high" + else: + return "critical" + + def calculate_file_complexity( + self, file_path: Path, content: Optional[str] = None + ) -> dict[str, any]: + """Calculate complexity for a single file.""" + if content is None: + try: + content = file_path.read_text(encoding="utf-8") + except Exception: + return { + "path": str(file_path), + "complexity": 0, + "rating": "unknown", + "error": "Could not read file", + } + + language = self._detect_language(file_path) + complexity = self.calculate(content, language) + + return { + "path": str(file_path), + "complexity": complexity, + "rating": self.get_rating(complexity), + "language": language, + } + + def _detect_language(self, file_path: Path) -> str: + """Detect language from file extension.""" + suffix = file_path.suffix.lower() + language_map = { + ".py": "python", + ".js": "javascript", + ".ts": "typescript", + ".go": "go", + ".rs": "rust", + ".java": "java", + } + return language_map.get(suffix, "python") + + def calculate_batch( + self, files: list[Path], contents: dict[str, str] + ) -> dict[str, dict[str, any]]: + """Calculate complexity for multiple files.""" + results = {} + for file_path in files: + content = contents.get(str(file_path)) + results[str(file_path)] = self.calculate_file_complexity(file_path, content) + return results