phpmetrics_check.py
- Pfad:
/var/www/tools/ki-protokoll/claude-hook/quality/phpmetrics_check.py - Namespace: claude-hook.quality
- Zeilen: 216 | Größe: 6,661 Bytes
- Geändert: 2025-12-28 14:18:16 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 89
- Dependencies: 60 (25%)
- LOC: 94 (20%)
- Methods: 100 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 100 (10%)
Keine Issues gefunden.
Dependencies 9
- use json
- use subprocess
- use sys
- use pathlib.Path
- use typing.Dict
- use typing.List
- use typing.Tuple
- use time
- use argparse
Funktionen 6
-
is_cache_valid()Zeile 49 -
load_metrics()Zeile 59 -
should_skip()Zeile 80 -
analyze_class()Zeile 89 -
run_analysis()Zeile 147 -
main()Zeile 179
Code
#!/usr/bin/env python3
"""
PHPMetrics Integration für Cohesion/Coupling-Analyse.
Läuft PHPMetrics auf der Codebase und erzeugt Warnungen
für Klassen mit schlechten Metriken.
Prinzip: Cohesion over Coupling (#13)
"""
import json
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Tuple
# =============================================================================
# KONFIGURATION
# =============================================================================
PHPMETRICS_BIN = "/opt/php-tools/vendor/bin/phpmetrics"
SRC_PATH = "/var/www/dev.campus.systemische-tools.de/src"
CACHE_FILE = "/tmp/phpmetrics_cache.json"
CACHE_MAX_AGE_SECONDS = 3600 # 1 Stunde
# Schwellwerte für Warnungen
THRESHOLDS = {
"lcom": 4, # Lack of Cohesion - höher = schlechter
"afferent_coupling": 10, # Wie viele Klassen nutzen diese
"efferent_coupling": 15, # Wie viele Klassen nutzt diese
"instability_low": 0.2, # Zu stabil (schwer zu ändern)
"instability_high": 0.8, # Zu instabil (zu viele Abhängigkeiten)
"ccn_method_max": 15, # Maximale Komplexität pro Methode
"wmc": 50, # Weighted Methods per Class
}
# Pfade die ignoriert werden
SKIP_PATHS = [
"/vendor/",
"/tests/",
"/Test/",
]
# =============================================================================
# HELPER
# =============================================================================
def is_cache_valid() -> bool:
"""Prüft ob der Cache noch gültig ist."""
cache = Path(CACHE_FILE)
if not cache.exists():
return False
import time
age = time.time() - cache.stat().st_mtime
return age < CACHE_MAX_AGE_SECONDS
def load_metrics() -> Dict:
"""Lädt Metriken aus Cache oder führt PHPMetrics aus."""
if is_cache_valid():
with open(CACHE_FILE) as f:
return json.load(f)
# PHPMetrics ausführen
result = subprocess.run(
[PHPMETRICS_BIN, "--report-json=" + CACHE_FILE, SRC_PATH],
capture_output=True,
text=True,
timeout=300
)
if result.returncode != 0 and not Path(CACHE_FILE).exists():
return {}
with open(CACHE_FILE) as f:
return json.load(f)
def should_skip(class_name: str) -> bool:
"""Prüft ob die Klasse übersprungen werden soll."""
return any(skip in class_name for skip in SKIP_PATHS)
# =============================================================================
# ANALYSE
# =============================================================================
def analyze_class(name: str, data: Dict) -> List[str]:
"""Analysiert eine Klasse und gibt Warnungen zurück."""
warnings = []
if not isinstance(data, dict):
return warnings
# LCOM - Lack of Cohesion of Methods
lcom = data.get("lcom", 0)
if lcom > THRESHOLDS["lcom"]:
warnings.append(
f"W13.1: {name} has low cohesion (LCOM={lcom}). "
"Consider splitting into focused classes."
)
# Afferent Coupling (incoming dependencies)
afferent = data.get("afferentCoupling", 0)
if afferent > THRESHOLDS["afferent_coupling"]:
warnings.append(
f"W13.2: {name} has high afferent coupling ({afferent} dependents). "
"Changes here affect many classes - consider interface."
)
# Efferent Coupling (outgoing dependencies)
efferent = data.get("efferentCoupling", 0)
if efferent > THRESHOLDS["efferent_coupling"]:
warnings.append(
f"W13.3: {name} has high efferent coupling ({efferent} dependencies). "
"Consider dependency injection or facade pattern."
)
# Instability
instability = data.get("instability", 0.5)
if instability < THRESHOLDS["instability_low"] and afferent > 5:
warnings.append(
f"W13.4: {name} is very stable (I={instability:.2f}) with many dependents. "
"Changes are risky - ensure good test coverage."
)
# Cyclomatic Complexity per Method
ccn_max = data.get("ccnMethodMax", 0)
if ccn_max > THRESHOLDS["ccn_method_max"]:
warnings.append(
f"W13.5: {name} has high method complexity (CCN={ccn_max}). "
"Consider extracting methods or simplifying logic."
)
# Weighted Methods per Class
wmc = data.get("wmc", 0)
if wmc > THRESHOLDS["wmc"]:
warnings.append(
f"W13.6: {name} has high overall complexity (WMC={wmc}). "
"Class is doing too much - consider splitting."
)
return warnings
def run_analysis() -> Tuple[List[str], Dict]:
"""Führt die vollständige Analyse durch."""
metrics = load_metrics()
all_warnings = []
stats = {
"classes_analyzed": 0,
"classes_with_warnings": 0,
"total_warnings": 0,
}
for name, data in metrics.items():
if should_skip(name):
continue
if not isinstance(data, dict):
continue
stats["classes_analyzed"] += 1
warnings = analyze_class(name, data)
if warnings:
stats["classes_with_warnings"] += 1
stats["total_warnings"] += len(warnings)
all_warnings.extend(warnings)
return all_warnings, stats
# =============================================================================
# MAIN
# =============================================================================
def main():
"""Hauptfunktion für CLI-Aufruf."""
import argparse
parser = argparse.ArgumentParser(description="PHPMetrics Cohesion Check")
parser.add_argument("--refresh", action="store_true", help="Cache neu erstellen")
parser.add_argument("--json", action="store_true", help="JSON-Ausgabe")
parser.add_argument("--class", dest="class_name", help="Nur eine Klasse prüfen")
args = parser.parse_args()
if args.refresh and Path(CACHE_FILE).exists():
Path(CACHE_FILE).unlink()
warnings, stats = run_analysis()
if args.json:
print(json.dumps({
"warnings": warnings,
"stats": stats
}, indent=2))
else:
print(f"PHPMetrics Analysis: {stats['classes_analyzed']} classes")
print(f"Classes with warnings: {stats['classes_with_warnings']}")
print(f"Total warnings: {stats['total_warnings']}")
print()
if warnings:
print("Warnings:")
for w in warnings[:20]: # Limit output
print(f" - {w}")
if len(warnings) > 20:
print(f" ... and {len(warnings) - 20} more")
if __name__ == "__main__":
main()