mirror of
https://github.com/luongnv89/claude-howto.git
synced 2026-06-05 22:36:34 +02:00
540508f392
- Add pre-commit hooks: Ruff lint/format, Bandit security scan, YAML/TOML validation - Add GitHub Actions CI workflow: lint, security, test, and build jobs - Configure Ruff and Bandit in pyproject.toml - Add pytest test suite for build_epub.py (25 tests) - Fix code issues: exception chaining, httpx timeout, formatting - Add requirements.txt and requirements-dev.txt
175 lines
5.6 KiB
Python
175 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Compare cyclomatic complexity of code before and after changes.
|
|
Helps identify if refactoring actually simplifies code structure.
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
|
|
|
|
class ComplexityAnalyzer:
|
|
"""Analyze code complexity metrics."""
|
|
|
|
def __init__(self, code: str):
|
|
self.code = code
|
|
self.lines = code.split("\n")
|
|
|
|
def calculate_cyclomatic_complexity(self) -> int:
|
|
"""
|
|
Calculate cyclomatic complexity using McCabe's method.
|
|
Count decision points: if, elif, else, for, while, except, and, or
|
|
"""
|
|
complexity = 1 # Base complexity
|
|
|
|
# Count decision points
|
|
decision_patterns = [
|
|
r"\bif\b",
|
|
r"\belif\b",
|
|
r"\bfor\b",
|
|
r"\bwhile\b",
|
|
r"\bexcept\b",
|
|
r"\band\b(?!$)",
|
|
r"\bor\b(?!$)",
|
|
]
|
|
|
|
for pattern in decision_patterns:
|
|
matches = re.findall(pattern, self.code)
|
|
complexity += len(matches)
|
|
|
|
return complexity
|
|
|
|
def calculate_cognitive_complexity(self) -> int:
|
|
"""
|
|
Calculate cognitive complexity - how hard is it to understand?
|
|
Based on nesting depth and control flow.
|
|
"""
|
|
cognitive = 0
|
|
nesting_depth = 0
|
|
|
|
for line in self.lines:
|
|
# Track nesting depth
|
|
if re.search(r"^\s*(if|for|while|def|class|try)\b", line):
|
|
nesting_depth += 1
|
|
cognitive += nesting_depth
|
|
elif re.search(r"^\s*(elif|else|except|finally)\b", line):
|
|
cognitive += nesting_depth
|
|
|
|
# Reduce nesting when unindenting
|
|
if line and not line[0].isspace():
|
|
nesting_depth = 0
|
|
|
|
return cognitive
|
|
|
|
def calculate_maintainability_index(self) -> float:
|
|
"""
|
|
Maintainability Index ranges from 0-100.
|
|
> 85: Excellent
|
|
> 65: Good
|
|
> 50: Fair
|
|
< 50: Poor
|
|
"""
|
|
lines = len(self.lines)
|
|
cyclomatic = self.calculate_cyclomatic_complexity()
|
|
cognitive = self.calculate_cognitive_complexity()
|
|
|
|
# Simplified MI calculation
|
|
mi = (
|
|
171
|
|
- 5.2 * (cyclomatic / lines)
|
|
- 0.23 * (cognitive)
|
|
- 16.2 * (lines / 1000)
|
|
)
|
|
|
|
return max(0, min(100, mi))
|
|
|
|
def get_complexity_report(self) -> dict:
|
|
"""Generate comprehensive complexity report."""
|
|
return {
|
|
"cyclomatic_complexity": self.calculate_cyclomatic_complexity(),
|
|
"cognitive_complexity": self.calculate_cognitive_complexity(),
|
|
"maintainability_index": round(self.calculate_maintainability_index(), 2),
|
|
"lines_of_code": len(self.lines),
|
|
"avg_line_length": round(
|
|
sum(len(l) for l in self.lines) / len(self.lines), 2
|
|
)
|
|
if self.lines
|
|
else 0,
|
|
}
|
|
|
|
|
|
def compare_files(before_file: str, after_file: str) -> None:
|
|
"""Compare complexity metrics between two code versions."""
|
|
|
|
with open(before_file) as f:
|
|
before_code = f.read()
|
|
|
|
with open(after_file) as f:
|
|
after_code = f.read()
|
|
|
|
before_analyzer = ComplexityAnalyzer(before_code)
|
|
after_analyzer = ComplexityAnalyzer(after_code)
|
|
|
|
before_metrics = before_analyzer.get_complexity_report()
|
|
after_metrics = after_analyzer.get_complexity_report()
|
|
|
|
print("=" * 60)
|
|
print("CODE COMPLEXITY COMPARISON")
|
|
print("=" * 60)
|
|
|
|
print("\nBEFORE:")
|
|
print(f" Cyclomatic Complexity: {before_metrics['cyclomatic_complexity']}")
|
|
print(f" Cognitive Complexity: {before_metrics['cognitive_complexity']}")
|
|
print(f" Maintainability Index: {before_metrics['maintainability_index']}")
|
|
print(f" Lines of Code: {before_metrics['lines_of_code']}")
|
|
print(f" Avg Line Length: {before_metrics['avg_line_length']}")
|
|
|
|
print("\nAFTER:")
|
|
print(f" Cyclomatic Complexity: {after_metrics['cyclomatic_complexity']}")
|
|
print(f" Cognitive Complexity: {after_metrics['cognitive_complexity']}")
|
|
print(f" Maintainability Index: {after_metrics['maintainability_index']}")
|
|
print(f" Lines of Code: {after_metrics['lines_of_code']}")
|
|
print(f" Avg Line Length: {after_metrics['avg_line_length']}")
|
|
|
|
print("\nCHANGES:")
|
|
cyclomatic_change = (
|
|
after_metrics["cyclomatic_complexity"] - before_metrics["cyclomatic_complexity"]
|
|
)
|
|
cognitive_change = (
|
|
after_metrics["cognitive_complexity"] - before_metrics["cognitive_complexity"]
|
|
)
|
|
mi_change = (
|
|
after_metrics["maintainability_index"] - before_metrics["maintainability_index"]
|
|
)
|
|
loc_change = after_metrics["lines_of_code"] - before_metrics["lines_of_code"]
|
|
|
|
print(f" Cyclomatic Complexity: {cyclomatic_change:+d}")
|
|
print(f" Cognitive Complexity: {cognitive_change:+d}")
|
|
print(f" Maintainability Index: {mi_change:+.2f}")
|
|
print(f" Lines of Code: {loc_change:+d}")
|
|
|
|
print("\nASSESSMENT:")
|
|
if mi_change > 0:
|
|
print(" ✅ Code is MORE maintainable")
|
|
elif mi_change < 0:
|
|
print(" ⚠️ Code is LESS maintainable")
|
|
else:
|
|
print(" ➡️ Maintainability unchanged")
|
|
|
|
if cyclomatic_change < 0:
|
|
print(" ✅ Complexity DECREASED")
|
|
elif cyclomatic_change > 0:
|
|
print(" ⚠️ Complexity INCREASED")
|
|
else:
|
|
print(" ➡️ Complexity unchanged")
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) != 3:
|
|
print("Usage: python compare-complexity.py <before_file> <after_file>")
|
|
sys.exit(1)
|
|
|
|
compare_files(sys.argv[1], sys.argv[2])
|