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