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