rules_quality.py

Code Hygiene Score: 93

Issues 2

Zeile Typ Beschreibung
112 magic_number Magic Number gefunden: 100
149 magic_number Magic Number gefunden: 100

Dependencies 7

Klassen 11

Code

#!/usr/bin/env python3
"""
Quality Rules (W1, W4, W6) - SRP, KISS, OOP, SOLID.

Regeln für Code-Qualität: Klassengröße, Single Responsibility,
OOP-Prinzipien, Encapsulation, Interface Segregation.
"""

import re
from pathlib import Path
from typing import List
from .rule_base import Rule, DTO_ALLOWLIST


# =============================================================================
# W1: SRP + KISS - Single Responsibility & Keep It Simple
# =============================================================================

class W1_1_ClassSize(Rule):
    """W1.1: Klassengröße (max 300 LOC)."""

    def check(self, file_path: str, content: str) -> List[str]:
        from .rule_base import count_non_empty_lines
        loc = count_non_empty_lines(content)
        warnings = []

        if loc > 300:
            warnings.append(f"W1.1: Class has {loc} lines (max 300). Consider splitting.")
        elif loc > 200:
            warnings.append(f"W1.1: Class has {loc} lines (approaching limit of 300).")

        return warnings


class W1_2_PublicMethodCount(Rule):
    """W1.2: Anzahl public methods (max 10)."""

    def check(self, file_path: str, content: str) -> List[str]:
        public_methods = re.findall(r"public\s+function\s+\w+", content)
        count = len(public_methods)

        if count > 10:
            return [f"W1.2: Class has {count} public methods (max 10). Consider splitting."]

        return []


class W1_3_ConstructorParams(Rule):
    """W1.3: Constructor-Parameter (max 5)."""

    def check(self, file_path: str, content: str) -> List[str]:
        constructor_match = re.search(r"function\s+__construct\s*\(([^)]*)\)", content, re.DOTALL)
        if not constructor_match:
            return []

        params = constructor_match.group(1)
        param_count = len([p for p in params.split(',') if p.strip()])

        if param_count > 5:
            return [f"W1.3: Constructor has {param_count} parameters (max 5). Consider refactoring."]

        return []


class W1_4_DependencyCount(Rule):
    """W1.4: Anzahl use-Statements (max 10)."""

    def check(self, file_path: str, content: str) -> List[str]:
        use_statements = re.findall(r"^use\s+", content, re.MULTILINE)
        count = len(use_statements)

        if count > 10:
            return [f"W1.4: Class has {count} dependencies (max 10). Consider reducing coupling."]

        return []


class W1_5_SuspiciousNames(Rule):
    """W1.5: Verdächtige Namen (Manager, And)."""

    def check(self, file_path: str, content: str) -> List[str]:
        filename = Path(file_path).stem
        hints = []

        if "Manager" in filename:
            hints.append(f"W1.5: Hint - '{filename}' contains 'Manager'. Verify single responsibility.")

        if re.search(r"[a-z]And[A-Z]", filename):
            hints.append(f"W1.5: Hint - '{filename}' contains 'And'. May indicate multiple responsibilities.")

        return hints


# =============================================================================
# W4: OOP - Object-Oriented Programming Principles
# =============================================================================

class W4_1_AnemicModel(Rule):
    """W4.1: Anämisches Model (hohe Accessor-Ratio > 70%)."""

    def __init__(self):
        super().__init__(allowlist=DTO_ALLOWLIST)

    def check(self, file_path: str, content: str) -> List[str]:
        getters = len(re.findall(r"public\s+function\s+get[A-Z]\w*\s*\(", content))
        setters = len(re.findall(r"public\s+function\s+set[A-Z]\w*\s*\(", content))
        all_public = len(re.findall(r"public\s+function\s+\w+", content))

        if all_public > 4:
            accessor_ratio = (getters + setters) / all_public
            if accessor_ratio > 0.7:
                return [f"W4.1: Potential anemic model: {int(accessor_ratio*100)}% accessors. Consider adding behavior."]

        return []


class W4_2_ClassWithoutBehavior(Rule):
    """W4.2: Klasse ohne Verhalten."""

    def __init__(self):
        super().__init__(allowlist=DTO_ALLOWLIST)

    def check(self, file_path: str, content: str) -> List[str]:
        has_properties = bool(re.search(r"(?:private|protected|public)\s+.*\$\w+", content))
        methods = re.findall(r"(?:public|private|protected)\s+function\s+(\w+)", content)
        real_methods = [m for m in methods if not m.startswith(("get", "set", "__"))]

        if has_properties and len(real_methods) == 0:
            return ["W4.2: Class has properties but no behavior methods."]

        return []


class W4_3_LowEncapsulation(Rule):
    """W4.3: Niedrige Kapselung (> 50% exposed properties)."""

    def __init__(self):
        super().__init__(allowlist=DTO_ALLOWLIST)

    def check(self, file_path: str, content: str) -> List[str]:
        public_props = len(re.findall(r"public\s+(?!function|const).*\$", content))
        protected_props = len(re.findall(r"protected\s+(?!function|const).*\$", content))
        private_props = len(re.findall(r"private\s+(?!function|const).*\$", content))

        total = public_props + protected_props + private_props
        if total > 3:
            exposed_ratio = (public_props + protected_props) / total
            if exposed_ratio > 0.5:
                return [f"W4.3: Low encapsulation: {int(exposed_ratio*100)}% exposed properties."]

        return []


class W4_4_HighStaticRatio(Rule):
    """W4.4: Hohe Static-Ratio (> 50% static methods)."""

    def check(self, file_path: str, content: str) -> List[str]:
        static_methods = len(re.findall(r"public\s+static\s+function", content))
        all_methods = len(re.findall(r"public\s+function", content))

        if all_methods > 0 and static_methods > 2:
            static_ratio = static_methods / all_methods
            if static_ratio > 0.5:
                return [f"W4.4: High static method ratio: {static_methods}/{all_methods}. Consider instance methods."]

        return []


# =============================================================================
# W6: SOLID + DIP - Interface Segregation Principle
# =============================================================================

class W6_1_InterfaceTooLarge(Rule):
    """W6.1: Interface mit zu vielen Methoden (max 7)."""

    def check(self, file_path: str, content: str) -> List[str]:
        interface_match = re.search(r"interface\s+(\w+)[^{]*\{", content)
        if not interface_match:
            return []

        interface_name = interface_match.group(1)
        interface_start = interface_match.end()
        interface_content = content[interface_start:]

        # Finde Interface-Ende
        brace_count = 1
        end_pos = 0
        for i, char in enumerate(interface_content):
            if char == '{':
                brace_count += 1
            elif char == '}':
                brace_count -= 1
                if brace_count == 0:
                    end_pos = i
                    break

        interface_body = interface_content[:end_pos]
        method_count = len(re.findall(r"function\s+\w+", interface_body))

        if method_count > 7:
            return [f"W6.1: Interface '{interface_name}' has {method_count} methods (max 7). Consider splitting (ISP)."]

        return []


class W6_2_TooManyInterfaces(Rule):
    """W6.2: Klasse implementiert zu viele Interfaces (max 5)."""

    def check(self, file_path: str, content: str) -> List[str]:
        implements_match = re.search(r"implements\s+([^{]+)", content)
        if not implements_match:
            return []

        interfaces = [i.strip() for i in implements_match.group(1).split(',') if i.strip()]
        count = len(interfaces)

        if count > 5:
            return [f"W6.2: Class implements {count} interfaces (max 5). Potential role confusion."]

        return []


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

RULES = [
    W1_1_ClassSize(),
    W1_2_PublicMethodCount(),
    W1_3_ConstructorParams(),
    W1_4_DependencyCount(),
    W1_5_SuspiciousNames(),
    W4_1_AnemicModel(),
    W4_2_ClassWithoutBehavior(),
    W4_3_LowEncapsulation(),
    W4_4_HighStaticRatio(),
    W6_1_InterfaceTooLarge(),
    W6_2_TooManyInterfaces(),
]
← Übersicht Graph