rules_leastsurprise.py

Code Hygiene Score: 98

Keine Issues gefunden.

Dependencies 4

Klassen 6

Code

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

W15.x Regeln: Warnt bei Code der sich anders verhält als erwartet.

Prinzip: "Code verhält sich so, wie Name und Struktur erwarten lassen. Keine Überraschungen."
"""

import re
from typing import List
from .rule_base import Rule


# =============================================================================
# W15: LEAST SURPRISE PRINCIPLE
# =============================================================================

class W15_1_GetterWithSideEffect(Rule):
    """W15.1: Getter mit Seiteneffekten - unerwartet bei lesenden Methoden."""

    # Seiteneffekt-Indikatoren
    SIDE_EFFECT_PATTERNS = [
        r"\bsave\s*\(",
        r"\bupdate\s*\(",
        r"\bdelete\s*\(",
        r"\binsert\s*\(",
        r"\bwrite\s*\(",
        r"\bpersist\s*\(",
        r"\bflush\s*\(",
        r"\bsend\s*\(",
        r"\bexecute\s*\(",
        r"\$this->\w+\s*=",  # Property-Änderung
        r"\$this->\w+\s*\[\s*\]\s*=",  # Array-Push
        r"\$this->\w+\s*\+\+",  # Increment
        r"\$this->\w+\s*--",  # Decrement
    ]

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

        # Finde alle get*-Methoden
        getter_pattern = r"function\s+(get[A-Z]\w*)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{"
        getter_matches = list(re.finditer(getter_pattern, content))

        for match in getter_matches:
            method_name = match.group(1)
            method_start = match.end()

            # Finde das Ende der Methode
            brace_count = 1
            pos = method_start
            while pos < len(content) and brace_count > 0:
                if content[pos] == '{':
                    brace_count += 1
                elif content[pos] == '}':
                    brace_count -= 1
                pos += 1

            method_body = content[method_start:pos]

            # Prüfe auf Seiteneffekte
            for side_effect_pattern in self.SIDE_EFFECT_PATTERNS:
                if re.search(side_effect_pattern, method_body):
                    warnings.append(
                        f"W15.1: Getter '{method_name}' appears to have side effects. "
                        f"Getters should be pure read operations."
                    )
                    break  # Nur eine Warnung pro Getter

        return warnings


class W15_2_SetterWithReturn(Rule):
    """W15.2: Setter mit Return-Wert - unerwartet bei schreibenden Methoden."""

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

        # Finde set*-Methoden mit Return-Type (außer self/static für fluent)
        pattern = r"function\s+(set[A-Z]\w*)\s*\([^)]+\)\s*:\s*(?!self|static|void|\$this)[A-Z]\w*"

        matches = re.findall(pattern, content)
        for method_name in matches:
            warnings.append(
                f"W15.2: Setter '{method_name}' has unexpected return type. "
                f"Setters should return void or self (for fluent interface)."
            )

        return warnings


class W15_3_BoolMethodWithoutQuestion(Rule):
    """W15.3: Boolean-Methode ohne is/has/can/should Präfix."""

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

        # Finde Methoden die bool zurückgeben
        pattern = r"function\s+(\w+)\s*\([^)]*\)\s*:\s*bool"

        matches = re.findall(pattern, content)
        for method_name in matches:
            # Erlaubte Präfixe für bool-Methoden
            if not re.match(r"^(is|has|can|should|will|was|does|did|are)", method_name):
                warnings.append(
                    f"W15.3: Boolean method '{method_name}' should start with "
                    f"is/has/can/should for clarity."
                )

        return warnings


class W15_4_ConstructorWithReturn(Rule):
    """W15.4: Constructor mit explizitem return - unerwartet."""

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

        # Finde __construct Methoden
        construct_match = re.search(
            r"function\s+__construct\s*\([^)]*\)\s*\{",
            content
        )

        if construct_match:
            construct_start = construct_match.end()

            # Finde das Ende des Constructors
            brace_count = 1
            pos = construct_start
            while pos < len(content) and brace_count > 0:
                if content[pos] == '{':
                    brace_count += 1
                elif content[pos] == '}':
                    brace_count -= 1
                pos += 1

            construct_body = content[construct_start:pos]

            # Prüfe auf return mit Wert (nicht nur 'return;')
            if re.search(r"\breturn\s+[^;]+;", construct_body):
                warnings.append(
                    "W15.4: Constructor has return statement with value. "
                    "Constructors should not return values."
                )

        return warnings


class W15_5_VoidMethodWithEcho(Rule):
    """W15.5: Void-Methode mit Echo - unerwartete Ausgabe."""

    def check(self, file_path: str, content: str) -> List[str]:
        # Überspringe Views und Templates
        if "/View/" in file_path or "/templates/" in file_path:
            return []

        warnings = []

        # Finde void-Methoden
        pattern = r"function\s+(\w+)\s*\([^)]*\)\s*:\s*void\s*\{"

        for match in re.finditer(pattern, content):
            method_name = match.group(1)
            method_start = match.end()

            # Finde das Ende der Methode
            brace_count = 1
            pos = method_start
            while pos < len(content) and brace_count > 0:
                if content[pos] == '{':
                    brace_count += 1
                elif content[pos] == '}':
                    brace_count -= 1
                pos += 1

            method_body = content[method_start:pos]

            # Prüfe auf echo/print
            if re.search(r"\b(echo|print)\b", method_body):
                warnings.append(
                    f"W15.5: Void method '{method_name}' produces output. "
                    f"Consider returning a value instead of echoing."
                )

        return warnings


class W15_6_MagicMethodOverride(Rule):
    """W15.6: Magic Methods mit unerwarteter Semantik."""

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

        # __toString sollte keine Exception werfen
        tostring_match = re.search(r"function\s+__toString\s*\([^)]*\)\s*:\s*string\s*\{", content)
        if tostring_match:
            method_start = tostring_match.end()
            brace_count = 1
            pos = method_start
            while pos < len(content) and brace_count > 0:
                if content[pos] == '{':
                    brace_count += 1
                elif content[pos] == '}':
                    brace_count -= 1
                pos += 1

            method_body = content[method_start:pos]

            if re.search(r"\bthrow\s+", method_body):
                warnings.append(
                    "W15.6: __toString() throws exception. This can cause hard-to-debug errors. "
                    "Return error string instead."
                )

        return warnings


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

RULES = [
    W15_1_GetterWithSideEffect(),
    W15_2_SetterWithReturn(),
    W15_3_BoolMethodWithoutQuestion(),
    W15_4_ConstructorWithReturn(),
    W15_5_VoidMethodWithEcho(),
    W15_6_MagicMethodOverride(),
]
← Übersicht Graph