pre_rules_validation.py

Code Hygiene Score: 98

Keine Issues gefunden.

Dependencies 6

Funktionen 4

Code

#!/usr/bin/env python3
"""
Pre-Hook Validation Regeln (BLOCK) - PSR + Types.

P3.x Regeln: strict_types, namespace, classname, return types
"""

import re
from pathlib import Path
from typing import Optional
from .rule_base import GLOBAL_ALLOWLIST, is_in_allowlist, block


# =============================================================================
# PRÜFUNG 3: PSR + Types
# =============================================================================

def p3_1_strict_types(file_path: str, content: str) -> Optional[dict]:
    """P3.1: strict_types erforderlich."""
    if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):
        return None

    strict_pattern = r"declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;"
    if not re.search(strict_pattern, content):
        return block("P3.1", "Missing declare(strict_types=1)")

    return None


def p3_2_namespace_matches_path(file_path: str, content: str) -> Optional[dict]:
    """P3.2: Namespace muss Pfad entsprechen."""
    if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):
        return None

    # Namespace-Mapping
    path_to_namespace = {
        "/src/Controller/": "Controller\\",
        "/src/Domain/": "Domain\\",
        "/src/UseCases/": "UseCases\\",
        "/src/Application/": "Application\\",
        "/src/Infrastructure/": "Infrastructure\\",
        "/src/Framework/": "Framework\\",
        "/app/": "App\\",
    }

    namespace_match = re.search(r"namespace\s+([^;]+);", content)
    if not namespace_match:
        return None  # Kein Namespace = kein Check

    declared_namespace = namespace_match.group(1).strip()

    for path_prefix, ns_prefix in path_to_namespace.items():
        if path_prefix in file_path:
            if not declared_namespace.startswith(ns_prefix.rstrip("\\")):
                return block("P3.2", f"Namespace '{declared_namespace}' does not match path. Expected: '{ns_prefix}...'")
            break

    return None


def p3_3_classname_matches_filename(file_path: str, content: str) -> Optional[dict]:
    """P3.3: Klassenname muss Dateiname entsprechen."""
    if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):
        return None

    class_match = re.search(r"(?:class|interface|trait|enum)\s+(\w+)", content)
    if not class_match:
        return None  # Keine Klasse = kein Check

    class_name = class_match.group(1)
    expected_name = Path(file_path).stem

    if class_name != expected_name:
        return block("P3.3", f"Class '{class_name}' does not match filename '{expected_name}'")

    return None


def p3_4_public_method_return_type(file_path: str, content: str) -> Optional[dict]:
    """P3.4: Public Methods müssen Return-Type haben."""
    if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):
        return None

    # Finde public functions
    public_methods = re.findall(
        r"public\s+function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\S+)?",
        content
    )

    for method in public_methods:
        if method.startswith("__"):
            continue  # Magic methods überspringen

        # Prüfe ob Return-Type vorhanden
        pattern = rf"public\s+function\s+{method}\s*\([^)]*\)\s*:\s*\S+"
        if not re.search(pattern, content):
            return block("P3.4", f"Public method '{method}' missing return type")

    return None


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

RULES = [
    p3_1_strict_types,
    p3_2_namespace_matches_path,
    p3_3_classname_matches_filename,
    p3_4_public_method_return_type,
]
← Übersicht