Initial upload with CI/CD workflow
This commit is contained in:
173
codesnap/utils/complexity.py
Normal file
173
codesnap/utils/complexity.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user