pre_rules_python.py

Code Hygiene Score: 100

Keine Issues gefunden.

Dependencies 4

Funktionen 2

Code

#!/usr/bin/env python3
"""
Pre-Hook Python Regeln (BLOCK) - Pipeline Code Quality.

PP1.x: Hardcoded Values
PP2.x: Config Patterns

Diese Regeln prüfen Python-Dateien in /var/www/scripts/pipeline/
auf häufige Fehler wie hardcoded Model-Namen oder Pipeline-IDs.
"""

import re
from typing import Optional
from .rule_base import block, is_in_allowlist


# =============================================================================
# ALLOWLIST
# =============================================================================

PYTHON_ALLOWLIST = [
    "/venv/",
    "/__pycache__/",
    "/tests/",
    "/test_",
    "_test.py",
]


# =============================================================================
# HARDCODED VALUES DETECTION
# =============================================================================

# Model-Namen die als hardcoded Default blockiert werden
HARDCODED_MODELS = {
    "mistral",
    "llama",
    "llama2",
    "llama3",
    "gemma",
    "gemma2",
    "gemma3",
    "phi",
    "phi3",
    "qwen",
    "qwen2",
    "claude",
    "gpt-4",
    "gpt-3.5",
    "minicpm",
    "minicpm-v",
    "nomic",
    "nomic-embed",
}


# =============================================================================
# PRÜFUNG PP1: HARDCODED VALUES
# =============================================================================

def pp1_1_hardcoded_model_default(file_path: str, content: str) -> Optional[dict]:
    """
    PP1.1: Hardcoded LLM Model-Namen als Default blockieren.

    BLOCKIERT:
        parser.add_argument("--model", default="mistral")
        def foo(model: str = "gemma"):

    ERLAUBT:
        parser.add_argument("--model", default=None)
        model = get_pipeline_model("step_type")
        DEFAULT_MODEL = "mistral"  # Als Konstante OK
        # Kommentar: mistral ist gut
    """
    if not file_path.endswith(".py"):
        return None
    if is_in_allowlist(file_path, PYTHON_ALLOWLIST):
        return None

    lines = content.split('\n')
    for line_num, line in enumerate(lines, 1):
        stripped = line.strip()

        # Skip Kommentare
        if stripped.startswith('#'):
            continue

        # Skip Docstrings (vereinfacht)
        if stripped.startswith('"""') or stripped.startswith("'''"):
            continue

        # Skip Konstanten-Definitionen (UPPER_CASE = "value")
        if re.match(r'^[A-Z][A-Z0-9_]*\s*=', stripped):
            continue

        # Skip Listen/Sets von erlaubten Werten
        if 'VALID_' in line or 'ALLOWED_' in line or 'HARDCODED_' in line:
            continue

        # Suche nach hardcoded model defaults
        for model in HARDCODED_MODELS:
            # Pattern 1: default="model" oder default='model'
            pattern1 = rf'default\s*=\s*["\']({model})["\']'
            # Pattern 2: model: str = "model" (type hint mit default)
            pattern2 = rf'model\s*:\s*str\s*=\s*["\']({model})["\']'
            # Pattern 3: model="model" als Keyword-Argument
            pattern3 = rf'\bmodel\s*=\s*["\']({model})["\']'

            for pattern in [pattern1, pattern2, pattern3]:
                match = re.search(pattern, line, re.IGNORECASE)
                if match:
                    return block(
                        "PP1.1",
                        f"Hardcoded model name '{model}' at line {line_num}. "
                        f"Use get_pipeline_model() to read from pipeline_steps config, "
                        f"or define as UPPER_CASE constant."
                    )

    return None


def pp1_2_hardcoded_pipeline_id(file_path: str, content: str) -> Optional[dict]:
    """
    PP1.2: Hardcoded Pipeline-IDs blockieren (außer als Konstante).

    BLOCKIERT:
        pipeline_id = 5
        run_pipeline(pipeline_id=3)

    ERLAUBT:
        DEFAULT_PIPELINE_ID = 5
        pipeline_id = args.pipeline_id
        pipeline_id = config.get("pipeline_id")
    """
    if not file_path.endswith(".py"):
        return None
    if is_in_allowlist(file_path, PYTHON_ALLOWLIST):
        return None

    lines = content.split('\n')
    for line_num, line in enumerate(lines, 1):
        stripped = line.strip()

        # Skip Kommentare
        if stripped.startswith('#'):
            continue

        # Skip Konstanten-Definitionen
        if re.match(r'^[A-Z][A-Z0-9_]*\s*=', stripped):
            continue

        # Skip wenn es eine Zuweisung von args/config ist
        if 'args.' in line or 'config.' in line or 'get(' in line:
            continue

        # Suche nach hardcoded pipeline_id
        # Pattern: pipeline_id = 5 oder pipeline_id=5 (nicht als Teil eines größeren Ausdrucks)
        pattern = r'\bpipeline_id\s*=\s*(\d+)\b'
        match = re.search(pattern, line)
        if match:
            pid = match.group(1)
            return block(
                "PP1.2",
                f"Hardcoded pipeline_id={pid} at line {line_num}. "
                f"Use DEFAULT_PIPELINE_ID constant or accept via parameter."
            )

    return None


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

RULES = [
    pp1_1_hardcoded_model_default,
    pp1_2_hardcoded_pipeline_id,
]
← Übersicht