rules_tradeoffs.py

Code Hygiene Score: 95

Issues 1

Zeile Typ Beschreibung
217 magic_number Magic Number gefunden: 100

Dependencies 4

Klassen 6

Code

#!/usr/bin/env python3
"""
Post-Hook Trade-off Documentation Regeln (WARN).

W10.x Regeln: Warnt bei Architektur-Mustern ohne Trade-off-Dokumentation.

Prinzip: "Make Trade-offs Explicit" (#10)

Diese Regeln erkennen komplexe Architektur-Entscheidungen und
warnen, wenn keine entsprechende Dokumentation vorhanden ist.
"""

import re
from typing import List
from .rule_base import Rule


# =============================================================================
# W10: TRADE-OFF DOCUMENTATION
# =============================================================================

class W10_1_AbstractFactoryWithoutDocs(Rule):
    """W10.1: Abstract Factory Pattern ohne @see ADR-Verweis."""

    def __init__(self):
        super().__init__(allowlist=[])

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

        # Erkennung: Abstract class mit Factory im Namen und create-Methoden
        is_factory = bool(re.search(
            r"abstract\s+class\s+\w*Factory",
            content
        )) or bool(re.search(
            r"interface\s+\w*Factory",
            content
        ))

        if is_factory:
            # Prüfe ob ADR-Referenz vorhanden
            has_adr_ref = bool(re.search(
                r"@see\s+ADR-\d+|ADR-\d+|Architecture Decision",
                content,
                re.IGNORECASE
            ))

            if not has_adr_ref:
                warnings.append(
                    "W10.1: Factory pattern without architecture documentation. "
                    "Add @see ADR-XXX reference explaining the design choice."
                )

        return warnings


class W10_2_EventDispatcherWithoutDocs(Rule):
    """W10.2: Event-basierte Architektur ohne Dokumentation."""

    EVENT_PATTERNS = [
        r"class\s+\w+Event\b",
        r"implements\s+\w*EventSubscriber",
        r"->dispatch\s*\(\s*new\s+\w+Event",
        r"EventDispatcher",
    ]

    def __init__(self):
        super().__init__(allowlist=[
            "/Framework/",  # Framework definiert Event-System
        ])

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

        has_events = any(
            re.search(pattern, content)
            for pattern in self.EVENT_PATTERNS
        )

        if has_events:
            has_docs = bool(re.search(
                r"@see\s+ADR|Event\s+Flow|Event-Dokumentation",
                content,
                re.IGNORECASE
            ))

            if not has_docs:
                warnings.append(
                    "W10.2: Event-based architecture without documentation. "
                    "Consider documenting event flow with ADR."
                )

        return warnings


class W10_3_CacheWithoutStrategy(Rule):
    """W10.3: Caching ohne dokumentierte Invalidation-Strategie."""

    CACHE_PATTERNS = [
        r"->cache\s*\(",
        r"Cache::",
        r"setCache\s*\(",
        r"apcu_",
        r"redis",
        r"memcache",
    ]

    def __init__(self):
        super().__init__(allowlist=[
            "/Infrastructure/Cache/",  # Cache-Infrastruktur selbst
        ])

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

        has_cache = any(
            re.search(pattern, content, re.IGNORECASE)
            for pattern in self.CACHE_PATTERNS
        )

        if has_cache:
            has_invalidation_docs = bool(re.search(
                r"invalidat|TTL|cache\s+strateg|@cache",
                content,
                re.IGNORECASE
            ))

            if not has_invalidation_docs:
                warnings.append(
                    "W10.3: Caching without invalidation strategy. "
                    "Document cache TTL and invalidation approach."
                )

        return warnings


class W10_4_MultipleInheritanceWithoutDocs(Rule):
    """W10.4: Trait-Nutzung ohne Dokumentation der Komposition."""

    def __init__(self):
        super().__init__(allowlist=[])

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

        # Zähle verwendete Traits
        trait_uses = re.findall(r"use\s+(\w+Trait)", content)

        if len(trait_uses) >= 3:
            has_composition_docs = bool(re.search(
                r"@composition|@uses|Trait-Komposition|Mixin",
                content,
                re.IGNORECASE
            ))

            if not has_composition_docs:
                warnings.append(
                    f"W10.4: Class uses {len(trait_uses)} traits without documentation. "
                    "Document trait composition and potential conflicts."
                )

        return warnings


class W10_5_ExternalServiceWithoutDocs(Rule):
    """W10.5: External Service Integration ohne Fehlerbehandlungs-Doku."""

    EXTERNAL_PATTERNS = [
        r"curl_",
        r"file_get_contents\s*\(\s*['\"]https?:",
        r"new\s+GuzzleHttp",
        r"new\s+HttpClient",
        r"->request\s*\(",
    ]

    def __init__(self):
        super().__init__(allowlist=[
            "/Infrastructure/External/",
            "/Infrastructure/Http/",
        ])

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

        has_external = any(
            re.search(pattern, content)
            for pattern in self.EXTERNAL_PATTERNS
        )

        if has_external:
            has_error_handling_docs = bool(re.search(
                r"@throws|timeout|retry|fallback|circuit.?breaker",
                content,
                re.IGNORECASE
            ))

            if not has_error_handling_docs:
                warnings.append(
                    "W10.5: External service call without error handling documentation. "
                    "Document timeout, retry, and fallback strategy."
                )

        return warnings


class W10_6_DeprecationWithoutMigration(Rule):
    """W10.6: @deprecated ohne Migrations-Hinweis."""

    def __init__(self):
        super().__init__(allowlist=[])

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

        # Finde @deprecated Annotations
        deprecated = re.findall(
            r"@deprecated\s+(.{0,100})",
            content,
            re.IGNORECASE
        )

        for deprecation_msg in deprecated:
            # Prüfe ob Migration dokumentiert ist
            has_migration = bool(re.search(
                r"use\s+\w+\s+instead|migrate\s+to|replacement|@see",
                deprecation_msg,
                re.IGNORECASE
            ))

            if not has_migration:
                warnings.append(
                    "W10.6: @deprecated without migration path. "
                    "Add 'Use XY instead' or '@see NewClass'."
                )
                break

        return warnings


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

RULES = [
    W10_1_AbstractFactoryWithoutDocs(),
    W10_2_EventDispatcherWithoutDocs(),
    W10_3_CacheWithoutStrategy(),
    W10_4_MultipleInheritanceWithoutDocs(),
    W10_5_ExternalServiceWithoutDocs(),
    W10_6_DeprecationWithoutMigration(),
]
← Übersicht Graph