rules_failsafe.py
- Pfad:
/var/www/tools/ki-protokoll/claude-hook/quality/rules_failsafe.py - Namespace: claude-hook.quality
- Zeilen: 291 | Größe: 8,855 Bytes
- Geändert: 2025-12-28 14:19:17 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 94
- Dependencies: 100 (25%)
- LOC: 69 (20%)
- Methods: 100 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 100 (10%)
Keine Issues gefunden.
Dependencies 4
- extends Rule
- use re
- use typing.List
- use rule_base.Rule
Klassen 6
-
W9_1_TryWithoutFinallyclass Zeile 24 -
W9_2_MissingExceptionHandlerclass Zeile 84 -
W9_3_CatchWithoutLoggingclass Zeile 127 -
W9_4_DestructorWithResourcesclass Zeile 170 -
W9_5_ExitWithoutCleanupclass Zeile 213 -
W9_6_ConnectionWithoutCloseclass Zeile 248
Code
#!/usr/bin/env python3
"""
Post-Hook Fail Safe Regeln (WARN) - Sichere Fehlerbehandlung.
W9.x Regeln: Warnt bei fehlendem Error-Recovery und Ressourcen-Cleanup.
Prinzip: "Wenn etwas fehlschlägt, soll es sicher fehlschlagen."
Fokus auf:
- Ressourcen-Cleanup (file handles, connections)
- Exception-Handler Vollständigkeit
- Graceful Degradation
"""
import re
from typing import List
from .rule_base import Rule
# =============================================================================
# W9: FAIL SAFE
# =============================================================================
class W9_1_TryWithoutFinally(Rule):
"""W9.1: try-Block mit Ressourcen-Handles ohne finally."""
# Ressourcen-Patterns die Cleanup benötigen
RESOURCE_PATTERNS = [
r"fopen\s*\(",
r"fsockopen\s*\(",
r"popen\s*\(",
r"curl_init\s*\(",
r"new\s+\w*Connection",
r"new\s+PDO\s*\(",
r"mysqli_connect\s*\(",
r"stream_socket_client\s*\(",
]
def __init__(self):
super().__init__(allowlist=[
"/Infrastructure/", # Infrastructure darf Ressourcen verwalten
"/Framework/",
])
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
# Finde try-Blöcke
try_blocks = re.findall(
r"try\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}\s*catch",
content,
re.DOTALL
)
for block in try_blocks:
# Prüfe ob Ressourcen geöffnet werden
has_resource = any(
re.search(pattern, block)
for pattern in self.RESOURCE_PATTERNS
)
if has_resource:
# Prüfe ob finally vorhanden ist
# Suche nach dem vollständigen try-catch-finally
block_start = content.find(block)
remaining = content[block_start + len(block):]
# Prüfe ob nach catch ein finally kommt
has_finally = bool(re.search(
r"catch\s*\([^)]+\)\s*\{[^}]*\}\s*finally\s*\{",
remaining[:500] # Schaue nur nahe nach
))
if not has_finally:
warnings.append(
"W9.1: try-block opens resources without finally. "
"Use try-finally for guaranteed cleanup."
)
break # Eine Warnung pro Datei reicht
return warnings
class W9_2_MissingExceptionHandler(Rule):
"""W9.2: Klasse mit Datenbank-Ops ohne Exception-Handling."""
DB_OPERATIONS = [
r"->execute\s*\(",
r"->query\s*\(",
r"->prepare\s*\(",
r"->fetch",
]
def __init__(self):
super().__init__(allowlist=[
"/Infrastructure/Persistence/", # Repos dürfen Exceptions werfen
"/Framework/",
])
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
# Hat die Datei DB-Operationen?
has_db_ops = any(
re.search(pattern, content)
for pattern in self.DB_OPERATIONS
)
if not has_db_ops:
return warnings
# Prüfe ob es einen try-catch gibt
has_try_catch = bool(re.search(r"try\s*\{", content))
# Prüfe ob die Klasse throws deklariert
has_throws = bool(re.search(r"@throws\s+\w*Exception", content))
if not has_try_catch and not has_throws:
warnings.append(
"W9.2: Database operations without exception handling. "
"Use try-catch or declare @throws for proper error propagation."
)
return warnings
class W9_3_CatchWithoutLogging(Rule):
"""W9.3: catch-Block ohne Logging (anders als W8.3 - hier geht es um Logging)."""
def __init__(self):
super().__init__(allowlist=[
"/tests/",
"/Test/",
])
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
# Finde catch-Blöcke mit Inhalt (nicht leer - das ist W8.1)
catch_blocks = re.findall(
r"catch\s*\(\s*\\?(?:\w+\\)*(\w+)\s+\$\w+\s*\)\s*\{([^{}]+)\}",
content,
re.DOTALL
)
for exception_type, block in catch_blocks:
# Skip wenn Block fast leer ist (W8.1 kümmert sich darum)
if len(block.strip()) < 10:
continue
# Prüfe ob Logging vorhanden
has_logging = bool(re.search(
r"->log\(|->error\(|->warning\(|error_log\(|Logger::|log\(",
block
))
# Prüfe ob Exception weitergereicht wird
has_rethrow = bool(re.search(r"throw\s+", block))
if not has_logging and not has_rethrow:
warnings.append(
f"W9.3: catch({exception_type}) without logging or rethrow. "
"Errors should be logged for debugging."
)
break # Eine Warnung pro Datei
return warnings
class W9_4_DestructorWithResources(Rule):
"""W9.4: __destruct ohne Ressourcen-Cleanup obwohl Properties vorhanden."""
RESOURCE_PROPERTIES = [
r"private\s+\?\?\w*\s*\$connection",
r"private\s+\?\?\w*\s*\$handle",
r"private\s+\?\?\w*\s*\$socket",
r"private\s+\?\?\w*\s*\$stream",
r"private\s+\?\?\w*\s*\$resource",
r"private\s+\?\?\w*\s*\$pdo",
r"private\s+\?\?\w*\s*\$mysqli",
]
def __init__(self):
super().__init__(allowlist=[])
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
# Hat die Klasse Ressourcen-Properties?
has_resource_props = any(
re.search(pattern, content, re.IGNORECASE)
for pattern in self.RESOURCE_PROPERTIES
)
if not has_resource_props:
return warnings
# Hat die Klasse einen __destruct?
has_destructor = bool(re.search(
r"(?:public|private|protected)\s+function\s+__destruct\s*\(",
content
))
if not has_destructor:
warnings.append(
"W9.4: Class has resource properties but no __destruct(). "
"Consider adding destructor for cleanup."
)
return warnings
class W9_5_ExitWithoutCleanup(Rule):
"""W9.5: exit/die ohne vorherigen Cleanup-Code."""
def __init__(self):
super().__init__(allowlist=[
"/bin/", # CLI-Scripts dürfen exit nutzen
"/scripts/",
"/public/index.php",
])
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
# Finde exit/die Aufrufe
exit_calls = re.findall(
r"(?:exit|die)\s*\([^)]*\)",
content
)
if len(exit_calls) > 0:
# Prüfe ob register_shutdown_function vorhanden
has_shutdown = bool(re.search(
r"register_shutdown_function\s*\(",
content
))
if not has_shutdown:
warnings.append(
f"W9.5: exit/die used ({len(exit_calls)}x) without shutdown handler. "
"Consider using register_shutdown_function for cleanup."
)
return warnings
class W9_6_ConnectionWithoutClose(Rule):
"""W9.6: Connection öffnen ohne schließen in derselben Methode."""
CONNECTION_OPEN = [
(r"fopen\s*\(", r"fclose\s*\("),
(r"curl_init\s*\(", r"curl_close\s*\("),
(r"popen\s*\(", r"pclose\s*\("),
]
def __init__(self):
super().__init__(allowlist=[
"/Infrastructure/", # Infrastructure verwaltet Connections
])
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
for open_pattern, close_pattern in self.CONNECTION_OPEN:
has_open = bool(re.search(open_pattern, content))
has_close = bool(re.search(close_pattern, content))
if has_open and not has_close:
resource_type = open_pattern.split(r"\s")[0].replace("\\", "")
warnings.append(
f"W9.6: {resource_type} without matching close. "
"Ensure resources are properly closed."
)
return warnings
# =============================================================================
# RULE COLLECTION
# =============================================================================
RULES = [
W9_1_TryWithoutFinally(),
W9_2_MissingExceptionHandler(),
W9_3_CatchWithoutLogging(),
W9_4_DestructorWithResources(),
W9_5_ExitWithoutCleanup(),
W9_6_ConnectionWithoutClose(),
]