rules_failfast.py

Code Hygiene Score: 100

Keine Issues gefunden.

Dependencies 5

Klassen 5

Code

#!/usr/bin/env python3
"""
Post-Hook Fail Fast Regeln (WARN) - Fail Fast Principle.

W8.x Regeln: Warnt bei stillen Fehlern, Error Suppression und leeren Catch-Blöcken.

Prinzip: "Ungültige Zustände werden sofort erkannt. Fehler werden früh sichtbar gemacht."
"""

import re
from typing import List
from .rule_base import Rule, GLOBAL_ALLOWLIST


# =============================================================================
# W8: FAIL FAST
# =============================================================================

class W8_1_EmptyCatch(Rule):
    """W8.1: Leerer catch-Block - Fehler werden verschluckt."""

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        # Leerer catch-Block: catch(...) { } oder catch(...) { // comment }
        # Pattern: catch mit leerem Body oder nur Whitespace/Kommentaren
        empty_catch_patterns = [
            # Komplett leer
            r"catch\s*\([^)]+\)\s*\{\s*\}",
            # Nur Whitespace
            r"catch\s*\([^)]+\)\s*\{\s+\}",
        ]

        for pattern in empty_catch_patterns:
            matches = re.findall(pattern, content)
            if matches:
                warnings.append(
                    f"W8.1: Empty catch block detected. Errors are silently swallowed. "
                    f"Log the exception or re-throw."
                )
                break

        return warnings


class W8_2_ErrorSuppression(Rule):
    """W8.2: Error Suppression mit @ Operator."""

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        # @ vor Variablen: @$var
        var_suppression = re.findall(r"@\s*\$\w+", content)

        # @ vor Funktionen: @function()
        func_suppression = re.findall(r"@\s*[a-zA-Z_]\w*\s*\(", content)

        total = len(var_suppression) + len(func_suppression)

        if total > 0:
            warnings.append(
                f"W8.2: Error suppression with @ operator ({total}x). "
                f"Handle errors explicitly instead of suppressing."
            )

        return warnings


class W8_3_CatchWithOnlyReturn(Rule):
    """W8.3: Catch-Block der nur return enthält - versteckt Fehlerursache."""

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        # catch(...) { return null; } oder { return false; } oder { return; }
        pattern = r"catch\s*\([^)]+\)\s*\{\s*return\s*(?:null|false|true|\d+)?;\s*\}"

        matches = re.findall(pattern, content, re.IGNORECASE)
        if matches:
            warnings.append(
                f"W8.3: Catch block only returns without logging ({len(matches)}x). "
                f"Consider logging the exception before returning."
            )

        return warnings


class W8_4_GenericException(Rule):
    """W8.4: Generische Exception ohne spezifischen Catch."""

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        # catch (Exception $e) oder catch (\Exception $e) ohne spezifischere catches davor
        generic_catch = re.findall(r"catch\s*\(\s*\\?Exception\s+\$\w+\s*\)", content)

        # Wenn generischer Catch, prüfe ob spezifische Catches vorhanden sind
        if generic_catch:
            specific_catch = re.findall(
                r"catch\s*\(\s*\\?(?!Exception\b)[A-Z]\w*Exception\s+\$\w+\s*\)",
                content
            )

            if not specific_catch:
                warnings.append(
                    f"W8.4: Only generic Exception catch without specific handlers. "
                    f"Consider catching specific exceptions first."
                )

        return warnings


class W8_5_ThrowInDestructor(Rule):
    """W8.5: Exception in Destructor - kann zu unerwartetem Verhalten führen."""

    def check(self, file_path: str, content: str) -> List[str]:
        warnings = []

        # Finde __destruct Methoden
        destruct_match = re.search(
            r"function\s+__destruct\s*\([^)]*\)\s*(?::\s*\w+)?\s*\{",
            content
        )

        if destruct_match:
            # Suche throw innerhalb des Destructors (vereinfacht)
            destruct_start = destruct_match.end()
            # Finde das Ende des Destructors
            brace_count = 1
            pos = destruct_start
            while pos < len(content) and brace_count > 0:
                if content[pos] == '{':
                    brace_count += 1
                elif content[pos] == '}':
                    brace_count -= 1
                pos += 1

            destruct_body = content[destruct_start:pos]

            if re.search(r"\bthrow\s+", destruct_body):
                warnings.append(
                    "W8.5: Exception thrown in __destruct(). "
                    "This can cause unexpected behavior. Catch and log instead."
                )

        return warnings


# =============================================================================
# RULE COLLECTION
# =============================================================================

RULES = [
    W8_1_EmptyCatch(),
    W8_2_ErrorSuppression(),
    W8_3_CatchWithOnlyReturn(),
    W8_4_GenericException(),
    W8_5_ThrowInDestructor(),
]
← Übersicht Graph