rules_failsafe.py

Code Hygiene Score: 94

Keine Issues gefunden.

Dependencies 4

Klassen 6

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(),
]
← Übersicht Graph