rule_evaluator.py

Code Hygiene Score: 99

Keine Issues gefunden.

Dependencies 3

Klassen 1

Code

"""
Rule Evaluator - Domain Service fuer Regel-Auswertung.

WICHTIG: Dies ist KEIN Repository!
Keine CRUD-Operationen, nur Business-Logik.
"""

import re
from typing import Any

from domain.contracts import ContractValidationResult


class RuleEvaluator:
    """Evaluiert Contract-Regeln gegen Code."""

    def evaluate_rule(
        self,
        file_path: str,
        content: str,
        rule: dict[str, Any],
        result: ContractValidationResult,
    ) -> None:
        """Prueft eine einzelne Rule gegen eine Datei."""
        check_type = rule.get("check_type", "")

        if check_type == "line_count":
            self._check_line_count(file_path, content, rule, result)
        elif check_type == "forbidden_pattern":
            self._check_forbidden_pattern(file_path, content, rule, result)
        elif check_type == "required_pattern":
            self._check_required_pattern(file_path, content, rule, result)
        elif check_type == "dependency_check":
            self._check_dependency(file_path, content, rule, result)

    def evaluate_legacy_forbidden(
        self,
        file_path: str,
        content: str,
        forbidden: list,
        result: ContractValidationResult,
    ) -> None:
        """Prueft auf verbotene Elemente (Legacy-Format)."""
        for item in forbidden:
            if isinstance(item, dict):
                element = item.get("element", "")
                severity = item.get("severity", "minor")
                message = item.get("message", f"Forbidden element found: {element}")
            else:
                element = str(item)
                severity = "minor"
                message = f"Forbidden element found: {element}"

            if element and element in content:
                self._add_violation(result, severity, {
                    "factor": "forbidden_element",
                    "file": file_path,
                    "message": message,
                })

    def evaluate_legacy_required(
        self,
        file_path: str,
        content: str,
        required: list,
        result: ContractValidationResult,
    ) -> None:
        """Prueft auf erforderliche Elemente (Legacy-Format)."""
        for item in required:
            if isinstance(item, dict):
                element = item.get("element", "")
                severity = item.get("severity", "major")
                message = item.get("message", f"Required element missing: {element}")
                applies_to = item.get("applies_to", [])
            else:
                element = str(item)
                severity = "major"
                message = f"Required element missing: {element}"
                applies_to = []

            if applies_to:
                matches = any(
                    file_path.endswith(p.lstrip("*")) for p in applies_to
                )
                if not matches:
                    continue

            if element and element not in content:
                self._add_violation(result, severity, {
                    "factor": "required_element",
                    "file": file_path,
                    "message": message,
                })

    def determine_outcome(self, result: ContractValidationResult) -> str:
        """Bestimmt das Validierungsergebnis basierend auf Violations."""
        if result.critical > 0:
            return "rejected"
        elif result.major > 2:
            return "revision_required"
        return "passed"

    def _check_line_count(
        self, file_path: str, content: str, rule: dict, result: ContractValidationResult
    ) -> None:
        """Prueft Zeilenanzahl einer Datei."""
        max_lines = rule.get("max_lines", 500)
        line_count = len(content.splitlines())

        if line_count > max_lines:
            self._add_violation(result, rule.get("severity", "major"), {
                "rule_id": rule.get("id", "line_count"),
                "factor": "line_count",
                "file": file_path,
                "message": f"File has {line_count} lines (max: {max_lines})",
                "actual": line_count,
                "limit": max_lines,
            })

    def _check_forbidden_pattern(
        self, file_path: str, content: str, rule: dict, result: ContractValidationResult
    ) -> None:
        """Prueft auf verbotene Patterns."""
        patterns = rule.get("patterns", [])

        for pattern in patterns:
            try:
                matches = re.findall(pattern, content)
            except re.error:
                matches = [pattern] if pattern in content else []

            if matches:
                line_no = self._find_line_number(content, pattern)
                self._add_violation(result, rule.get("severity", "major"), {
                    "rule_id": rule.get("id", "forbidden_pattern"),
                    "factor": "forbidden_pattern",
                    "file": file_path,
                    "message": f"Forbidden pattern found: '{pattern}'",
                    "description": rule.get("description", ""),
                    "line": line_no,
                    "occurrences": len(matches),
                })

    def _check_required_pattern(
        self, file_path: str, content: str, rule: dict, result: ContractValidationResult
    ) -> None:
        """Prueft auf erforderliche Patterns."""
        patterns = rule.get("patterns", [])

        for pattern in patterns:
            try:
                found = bool(re.search(pattern, content))
            except re.error:
                found = pattern in content

            if not found:
                self._add_violation(result, rule.get("severity", "major"), {
                    "rule_id": rule.get("id", "required_pattern"),
                    "factor": "required_pattern",
                    "file": file_path,
                    "message": f"Required pattern not found: '{pattern}'",
                    "description": rule.get("description", ""),
                })

    def _check_dependency(
        self, file_path: str, content: str, rule: dict, result: ContractValidationResult
    ) -> None:
        """Prueft auf verbotene Dependencies/Imports."""
        forbidden_imports = rule.get("forbidden_imports", [])

        for forbidden in forbidden_imports:
            php_pattern = rf"use\s+.*{re.escape(forbidden)}"
            py_pattern = rf"(from|import)\s+.*{re.escape(forbidden)}"

            try:
                if re.search(php_pattern, content) or re.search(py_pattern, content):
                    self._add_violation(result, rule.get("severity", "critical"), {
                        "rule_id": rule.get("id", "dependency_check"),
                        "factor": "forbidden_dependency",
                        "file": file_path,
                        "message": f"Forbidden import/dependency: '{forbidden}'",
                    })
            except re.error:
                pass

    def _find_line_number(self, content: str, pattern: str) -> int | None:
        """Findet die Zeilennummer eines Patterns."""
        for i, line in enumerate(content.splitlines(), 1):
            if pattern in line:
                return i
            try:
                if re.search(pattern, line):
                    return i
            except re.error:
                pass
        return None

    def _add_violation(
        self, result: ContractValidationResult, severity: str, finding: dict
    ) -> None:
        """Fuegt eine Violation zum Result hinzu."""
        finding["type"] = severity

        if severity == "critical":
            result.critical += 1
        elif severity == "major":
            result.major += 1
        else:
            result.minor += 1

        result.findings.append(finding)
← Übersicht Graph