Add graph, analyzers, and exporters modules
This commit is contained in:
165
src/analyzers/complexity.py
Normal file
165
src/analyzers/complexity.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.parsers.base import Entity, EntityType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ComplexityReport:
|
||||||
|
file_path: Optional[Path] = None
|
||||||
|
functions: list[dict] = field(default_factory=list)
|
||||||
|
classes: list[dict] = field(default_factory=list)
|
||||||
|
total_cyclomatic_complexity: int = 0
|
||||||
|
average_complexity: float = 0.0
|
||||||
|
high_complexity_functions: list[dict] = field(default_factory=list)
|
||||||
|
warnings: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class ComplexityCalculator:
|
||||||
|
DECISION_POINTS = {"if_statement", "elif_statement", "else_clause", "for_statement",
|
||||||
|
"while_statement", "case_clause", "ternary_expression"}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.complexity_threshold = 10
|
||||||
|
|
||||||
|
def calculate_for_entity(self, entity: Entity) -> dict:
|
||||||
|
code = entity.code
|
||||||
|
|
||||||
|
base_complexity = 1
|
||||||
|
|
||||||
|
for_decision_points = self._count_decision_points(code)
|
||||||
|
|
||||||
|
complexity = base_complexity + for_decision_points
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": entity.name,
|
||||||
|
"entity_type": entity.entity_type.value,
|
||||||
|
"file_path": str(entity.file_path),
|
||||||
|
"start_line": entity.start_line,
|
||||||
|
"end_line": entity.end_line,
|
||||||
|
"complexity_score": complexity,
|
||||||
|
"decision_points": for_decision_points,
|
||||||
|
"lines_of_code": entity.end_line - entity.start_line + 1,
|
||||||
|
"is_complex": complexity > self.complexity_threshold,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _count_decision_points(self, code: str) -> int:
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
if "if " in code:
|
||||||
|
if_count = code.count("if ")
|
||||||
|
if_count += code.count("elif ")
|
||||||
|
count += if_count
|
||||||
|
|
||||||
|
if " for " in code or "for " in code:
|
||||||
|
count += code.count("for ")
|
||||||
|
|
||||||
|
if " while " in code or "while " in code:
|
||||||
|
count += code.count("while ")
|
||||||
|
|
||||||
|
if " case " in code or "case:" in code:
|
||||||
|
count += code.count("case ")
|
||||||
|
|
||||||
|
count += code.count("? ") if "?" in code else 0
|
||||||
|
|
||||||
|
count += code.count(" and ") if " and " in code else 0
|
||||||
|
count += code.count(" or ") if " or " in code else 0
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
def calculate_for_file(self, file_path: Path, entities: list[Entity]) -> ComplexityReport:
|
||||||
|
report = ComplexityReport(file_path=file_path)
|
||||||
|
|
||||||
|
for entity in entities:
|
||||||
|
if entity.entity_type in [EntityType.FUNCTION, EntityType.METHOD]:
|
||||||
|
entity_complexity = self.calculate_for_entity(entity)
|
||||||
|
report.functions.append(entity_complexity)
|
||||||
|
report.total_cyclomatic_complexity += entity_complexity["complexity_score"]
|
||||||
|
|
||||||
|
if entity_complexity["is_complex"]:
|
||||||
|
report.high_complexity_functions.append(entity_complexity)
|
||||||
|
|
||||||
|
elif entity.entity_type == EntityType.CLASS:
|
||||||
|
class_info = {
|
||||||
|
"name": entity.name,
|
||||||
|
"file_path": str(entity.file_path),
|
||||||
|
"start_line": entity.start_line,
|
||||||
|
"end_line": entity.end_line,
|
||||||
|
"methods_count": len(entity.children),
|
||||||
|
"total_complexity": sum(
|
||||||
|
self.calculate_for_entity(child)["complexity_score"]
|
||||||
|
for child in entity.children
|
||||||
|
if child.entity_type == EntityType.METHOD
|
||||||
|
),
|
||||||
|
}
|
||||||
|
report.classes.append(class_info)
|
||||||
|
report.total_cyclomatic_complexity += class_info["total_complexity"]
|
||||||
|
|
||||||
|
if report.functions:
|
||||||
|
complexities = [f["complexity_score"] for f in report.functions]
|
||||||
|
report.average_complexity = sum(complexities) / len(complexities)
|
||||||
|
|
||||||
|
if report.high_complexity_functions:
|
||||||
|
report.warnings.append(
|
||||||
|
f"Found {len(report.high_complexity_functions)} functions with high complexity"
|
||||||
|
)
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
def calculate_project_complexity(self, entities: list[Entity]) -> dict:
|
||||||
|
function_complexities = []
|
||||||
|
class_complexities = []
|
||||||
|
|
||||||
|
for entity in entities:
|
||||||
|
if entity.entity_type in [EntityType.FUNCTION, EntityType.METHOD]:
|
||||||
|
complexity = self.calculate_for_entity(entity)
|
||||||
|
function_complexities.append(complexity)
|
||||||
|
|
||||||
|
elif entity.entity_type == EntityType.CLASS:
|
||||||
|
class_info = {
|
||||||
|
"name": entity.name,
|
||||||
|
"file_path": str(entity.file_path),
|
||||||
|
"methods": [
|
||||||
|
self.calculate_for_entity(child)
|
||||||
|
for child in entity.children
|
||||||
|
if child.entity_type == EntityType.METHOD
|
||||||
|
],
|
||||||
|
}
|
||||||
|
class_complexities.append(class_info)
|
||||||
|
|
||||||
|
total_complexity = sum(f["complexity_score"] for f in function_complexities)
|
||||||
|
avg_complexity = (
|
||||||
|
total_complexity / len(function_complexities)
|
||||||
|
if function_complexities
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_functions": len(function_complexities),
|
||||||
|
"total_classes": len(class_complexities),
|
||||||
|
"total_cyclomatic_complexity": total_complexity,
|
||||||
|
"average_complexity": round(avg_complexity, 2),
|
||||||
|
"high_complexity_count": sum(1 for f in function_complexities if f["is_complex"]),
|
||||||
|
"functions": function_complexities[:20],
|
||||||
|
"complexity_distribution": self._get_complexity_distribution(function_complexities),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_complexity_distribution(self, complexities: list[dict]) -> dict:
|
||||||
|
distribution = {"low": 0, "medium": 0, "high": 0, "very_high": 0}
|
||||||
|
|
||||||
|
for c in complexities:
|
||||||
|
score = c["complexity_score"]
|
||||||
|
if score <= 5:
|
||||||
|
distribution["low"] += 1
|
||||||
|
elif score <= 10:
|
||||||
|
distribution["medium"] += 1
|
||||||
|
elif score <= 20:
|
||||||
|
distribution["high"] += 1
|
||||||
|
else:
|
||||||
|
distribution["very_high"] += 1
|
||||||
|
|
||||||
|
return distribution
|
||||||
|
|
||||||
|
def set_complexity_threshold(self, threshold: int) -> None:
|
||||||
|
self.complexity_threshold = threshold
|
||||||
Reference in New Issue
Block a user