Backup #1804

ID1804
Dateipfad/var/www/dev.campus.systemische-tools.de/src/Infrastructure/CodeAnalysis/CodeQualityChecker.php
Version9
Typ modified
Größe11.6 KB
Hashad909a083c268d1e60d470d626afdc4780fa9564113aa74b9b6be0004562c6c7
Datum2025-12-27 15:17:47
Geändert vonclaude-code-hook
GrundClaude Code Pre-Hook Backup vor Edit-Operation
Datei existiert Ja

Dateiinhalt

<?php

declare(strict_types=1);

namespace Infrastructure\CodeAnalysis;

// @responsibility: Code-Hygiene-Analyse (Score 0-100, normalisiert, gewichtet)

final class CodeQualityChecker
{
    /** @var array<string, string> Regex patterns for secret detection */
    private const SECRET_PATTERNS = [
        'password' => '/["\'](?:password|passwd|pwd)["\']\\s*[=:]\\s*["\'][^"\']{3,}["\']/i',
        'api_key' => '/["\'](?:api[_-]?key|apikey|secret[_-]?key)["\']\\s*[=:]\\s*["\'][^"\']{8,}["\']/i',
        'token' => '/["\'](?:token|auth[_-]?token|access[_-]?token)["\']\\s*[=:]\\s*["\'][^"\']{8,}["\']/i',
        'url_with_creds' => '/https?:\\/\\/[^:]+:[^@]+@/i',
    ];

    /** @var array<string, string> Regex patterns for magic number detection */
    private const MAGIC_NUMBER_PATTERNS = [
        'magic_number' => '/(?<![\\w])(?:100|1000|60|24|365|3600|86400)(?![\\w])/',
    ];

    /** @var array<string, array{optimal: int, max: int}> Default hygiene thresholds */
    private const HYGIENE_DEFAULTS = [
        'loc' => ['optimal' => 200, 'max' => 500],
        'methods' => ['optimal' => 10, 'max' => 20],
        'classes' => ['optimal' => 1, 'max' => 3],
        'dependencies' => ['optimal' => 5, 'max' => 15],
        'secrets' => ['optimal' => 0, 'max' => 1],
        'magic_numbers' => ['optimal' => 0, 'max' => 10],
    ];

    /** @var array<string, array<string, array{optimal: int, max: int}>> File type specific modifiers */
    private const FILE_TYPE_MODIFIERS = [
        'Controller' => [
            'loc' => ['optimal' => 300, 'max' => 500],
            'methods' => ['optimal' => 15, 'max' => 25],
        ],
        'Entity' => [
            'loc' => ['optimal' => 100, 'max' => 300],
            'methods' => ['optimal' => 20, 'max' => 30],
        ],
        'Repository' => [
            'loc' => ['optimal' => 250, 'max' => 400],
            'dependencies' => ['optimal' => 3, 'max' => 8],
        ],
        'Service' => [
            'loc' => ['optimal' => 150, 'max' => 350],
        ],
        'UseCase' => [
            'loc' => ['optimal' => 150, 'max' => 350],
        ],
    ];

    /** @var array<string, float> Factor weights (must sum to 1.0) */
    private const WEIGHTS = [
        'dependencies' => 0.25,
        'loc' => 0.20,
        'methods' => 0.20,
        'secrets' => 0.15,
        'classes' => 0.10,
        'magic_numbers' => 0.10,
    ];

    /**
     * Analysiert eine Datei und berechnet den Code Hygiene Score.
     *
     * @param array<string, mixed> $analysisData Daten aus code_analysis
     * @return array{
     *     hygiene_score: int,
     *     factor_scores: array<string, int>,
     *     issues_count: int,
     *     warnings_count: int,
     *     issues_json: string
     * }
     */
    public function analyze(array $analysisData): array
    {
        $issues = [];
        $filePath = $analysisData['file_path'] ?? '';
        $content = $analysisData['file_content'] ?? '';

        // Determine file type for modifiers
        $fileType = $this->detectFileType($filePath);
        $thresholds = $this->getThresholdsForType($fileType);

        // Extract metrics
        $metrics = [
            'loc' => (int) ($analysisData['line_count'] ?? 0),
            'methods' => $this->countMethods($analysisData),
            'classes' => (int) ($analysisData['class_count'] ?? 1),
            'dependencies' => $this->countDependencies($analysisData),
            'secrets' => 0,
            'magic_numbers' => 0,
        ];

        // Detect secrets and magic numbers
        $secretIssues = $this->detectSecrets($content, $filePath);
        $metrics['secrets'] = count($secretIssues);
        $issues = array_merge($issues, $secretIssues);

        $magicIssues = $this->detectMagicNumbers($content, $filePath);
        $metrics['magic_numbers'] = count($magicIssues);
        $issues = array_merge($issues, $magicIssues);

        // Add threshold violation issues
        $issues = array_merge($issues, $this->detectThresholdViolations($metrics, $thresholds));

        // Calculate normalized factor scores
        $factorScores = [];
        foreach (self::WEIGHTS as $factor => $weight) {
            $factorScores[$factor] = $this->normalize(
                $metrics[$factor],
                $thresholds[$factor]['optimal'],
                $thresholds[$factor]['max']
            );
        }

        // Calculate weighted hygiene score
        $hygieneScore = $this->calculateWeightedScore($factorScores);

        // Apply hard fail for secrets
        if ($metrics['secrets'] > 0) {
            $hygieneScore = min($hygieneScore, 20);
        }

        // Count warnings
        $warningsCount = count(array_filter($issues, fn ($i) => $i['severity'] === 'warning'));

        return [
            'hygiene_score' => $hygieneScore,
            'factor_scores' => $factorScores,
            'issues_count' => count($issues),
            'warnings_count' => $warningsCount,
            'issues_json' => json_encode($issues, JSON_UNESCAPED_UNICODE),
        ];
    }

    /**
     * Normalisiert einen Wert auf 0-100 Skala.
     * Formel: max(0, 100 - ((value - optimal) / (max - optimal)) * 100)
     */
    private function normalize(int $value, int $optimal, int $max): int
    {
        if ($value <= $optimal) {
            return 100;
        }

        if ($value >= $max) {
            return 0;
        }

        $range = $max - $optimal;
        if ($range === 0) {
            return 0;
        }

        $normalized = 100 - (($value - $optimal) / $range) * 100;

        return (int) max(0, min(100, $normalized));
    }

    /**
     * Berechnet den gewichteten Gesamtscore.
     *
     * @param array<string, int> $factorScores
     */
    private function calculateWeightedScore(array $factorScores): int
    {
        $weightedSum = 0.0;

        foreach (self::WEIGHTS as $factor => $weight) {
            $weightedSum += ($factorScores[$factor] ?? 0) * $weight;
        }

        return (int) round($weightedSum);
    }

    /**
     * Ermittelt den Dateityp aus dem Pfad.
     */
    private function detectFileType(string $filePath): ?string
    {
        $filename = basename($filePath);

        foreach (array_keys(self::FILE_TYPE_MODIFIERS) as $type) {
            if (str_contains($filename, $type)) {
                return $type;
            }
        }

        // Check directory
        if (str_contains($filePath, '/Controller/')) {
            return 'Controller';
        }
        if (str_contains($filePath, '/Entity/')) {
            return 'Entity';
        }
        if (str_contains($filePath, '/Repository/') || str_contains($filePath, '/Persistence/')) {
            return 'Repository';
        }
        if (str_contains($filePath, '/Service/')) {
            return 'Service';
        }
        if (str_contains($filePath, '/UseCase/') || str_contains($filePath, '/UseCases/')) {
            return 'UseCase';
        }

        return null;
    }

    /**
     * Gibt die Thresholds für einen Dateityp zurück.
     *
     * @return array<string, array{optimal: int, max: int}>
     */
    private function getThresholdsForType(?string $fileType): array
    {
        $thresholds = self::HYGIENE_DEFAULTS;

        if ($fileType !== null && isset(self::FILE_TYPE_MODIFIERS[$fileType])) {
            foreach (self::FILE_TYPE_MODIFIERS[$fileType] as $factor => $values) {
                $thresholds[$factor] = $values;
            }
        }

        return $thresholds;
    }

    /**
     * Zählt die Methoden aus den Analysedaten.
     *
     * @param array<string, mixed> $analysisData
     */
    private function countMethods(array $analysisData): int
    {
        $functions = json_decode($analysisData['functions'] ?? '[]', true);

        return is_array($functions) ? count($functions) : 0;
    }

    /**
     * Zählt die Dependencies aus den Analysedaten.
     *
     * @param array<string, mixed> $analysisData
     */
    private function countDependencies(array $analysisData): int
    {
        $uses = json_decode($analysisData['uses'] ?? '[]', true);

        return is_array($uses) ? count($uses) : 0;
    }

    /**
     * Erkennt Secrets im Code (Hard Fail).
     *
     * @return array<array{type: string, rule: string, message: string, severity: string, line?: int}>
     */
    private function detectSecrets(string $content, string $filePath): array
    {
        $issues = [];
        $filename = basename($filePath);

        // Skip config files
        if (preg_match('/^(config|\.env|test|spec)/i', $filename)) {
            return [];
        }

        foreach (self::SECRET_PATTERNS as $type => $pattern) {
            if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
                foreach ($matches[0] as $match) {
                    $line = substr_count(substr($content, 0, $match[1]), "\n") + 1;
                    $issues[] = [
                        'type' => 'secret',
                        'rule' => "hardcoded-{$type}",
                        'message' => "KRITISCH: Mögliches hardcoded {$type} gefunden",
                        'severity' => 'critical',
                        'line' => $line,
                    ];
                }
            }
        }

        return $issues;
    }

    /**
     * Erkennt Magic Numbers im Code.
     *
     * @return array<array{type: string, rule: string, message: string, severity: string, line?: int}>
     */
    private function detectMagicNumbers(string $content, string $filePath): array
    {
        $issues = [];

        // Skip config/constant files
        if (preg_match('/const|config/i', $filePath)) {
            return [];
        }

        foreach (self::MAGIC_NUMBER_PATTERNS as $type => $pattern) {
            if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
                foreach ($matches[0] as $match) {
                    $line = substr_count(substr($content, 0, $match[1]), "\n") + 1;
                    $issues[] = [
                        'type' => 'magic_number',
                        'rule' => 'hardcoded-magic-number',
                        'message' => "Magic Number gefunden: {$match[0]}",
                        'severity' => 'info',
                        'line' => $line,
                    ];
                }
            }
        }

        return $issues;
    }

    /**
     * Erkennt Threshold-Verletzungen.
     *
     * @param array<string, int> $metrics
     * @param array<string, array{optimal: int, max: int}> $thresholds
     * @return array<array{type: string, rule: string, message: string, severity: string}>
     */
    private function detectThresholdViolations(array $metrics, array $thresholds): array
    {
        $issues = [];

        if ($metrics['loc'] > $thresholds['loc']['max']) {
            $issues[] = [
                'type' => 'complexity',
                'rule' => 'file-too-long',
                'message' => "Datei hat {$metrics['loc']} Zeilen (max: {$thresholds['loc']['max']})",
                'severity' => 'warning',
            ];
        }

        if ($metrics['methods'] > $thresholds['methods']['max']) {
            $issues[] = [
                'type' => 'srp',
                'rule' => 'too-many-methods',
                'message' => "Klasse hat {$metrics['methods']} Methoden (max: {$thresholds['methods']['max']})",
                'severity' => 'warning',
            ];
        }

        if ($metrics['dependencies'] > $thresholds['dependencies']['max']) {
            $issues[] = [
                'type' => 'coupling',
                'rule' => 'too-many-dependencies',
                'message' => "Klasse hat {$metrics['dependencies']} Dependencies (max: {$thresholds['dependencies']['max']})",
                'severity' => 'warning',
            ];
        }

        return $issues;
    }
}

Vollständig herunterladen

Aktionen

Herunterladen

Andere Versionen dieser Datei

ID Version Typ Größe Datum
1806 11 modified 13.1 KB 2025-12-27 15:29
1805 10 modified 11.6 KB 2025-12-27 15:28
1804 9 modified 11.6 KB 2025-12-27 15:17
1325 8 modified 12.0 KB 2025-12-25 16:24
1324 7 modified 12.1 KB 2025-12-25 16:24
1323 6 modified 12.5 KB 2025-12-25 16:24
1308 5 modified 7.9 KB 2025-12-25 16:14
1113 4 modified 7.9 KB 2025-12-25 09:23
1030 3 modified 7.9 KB 2025-12-25 00:21
1029 2 modified 7.8 KB 2025-12-25 00:21
964 1 modified 7.8 KB 2025-12-23 22:29

← Zurück zur Übersicht