From 84cc89a503df43f1ee77a2681af1b279c4b46ffe Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Mon, 2 Feb 2026 02:40:23 +0000 Subject: [PATCH] Add graph, analyzers, and exporters modules --- src/analyzers/complexity.py | 165 ++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 src/analyzers/complexity.py diff --git a/src/analyzers/complexity.py b/src/analyzers/complexity.py new file mode 100644 index 0000000..4e9e0e4 --- /dev/null +++ b/src/analyzers/complexity.py @@ -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