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