{
"tool_response": {
"type": "update",
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Infrastructure\/CodeAnalysis\/CodeQualityChecker.php",
"content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Infrastructure\\CodeAnalysis;\n\n\/\/ @responsibility: Code-Hygiene-Analyse (Score 0-100, normalisiert, gewichtet)\n\nfinal class CodeQualityChecker\n{\n \/** @var array<string, string> Regex patterns for secret detection *\/\n private const SECRET_PATTERNS = [\n 'password' => '\/[\"\\'](?:password|passwd|pwd)[\"\\']\\\\s*[=:]\\\\s*[\"\\'][^\"\\']{3,}[\"\\']\/i',\n 'api_key' => '\/[\"\\'](?:api[_-]?key|apikey|secret[_-]?key)[\"\\']\\\\s*[=:]\\\\s*[\"\\'][^\"\\']{8,}[\"\\']\/i',\n 'token' => '\/[\"\\'](?:token|auth[_-]?token|access[_-]?token)[\"\\']\\\\s*[=:]\\\\s*[\"\\'][^\"\\']{8,}[\"\\']\/i',\n 'url_with_creds' => '\/https?:\\\\\/\\\\\/[^:]+:[^@]+@\/i',\n ];\n\n \/** @var array<string, string> Regex patterns for magic number detection *\/\n private const MAGIC_NUMBER_PATTERNS = [\n 'magic_number' => '\/(?<![\\\\w])(?:100|1000|60|24|365|3600|86400)(?![\\\\w])\/',\n ];\n\n \/** @var array<string, array{optimal: int, max: int}> Default hygiene thresholds *\/\n private const HYGIENE_DEFAULTS = [\n 'loc' => ['optimal' => 200, 'max' => 500],\n 'methods' => ['optimal' => 10, 'max' => 20],\n 'classes' => ['optimal' => 1, 'max' => 3],\n 'dependencies' => ['optimal' => 5, 'max' => 15],\n 'secrets' => ['optimal' => 0, 'max' => 1],\n 'magic_numbers' => ['optimal' => 0, 'max' => 10],\n ];\n\n \/** @var array<string, array<string, array{optimal: int, max: int}>> File type specific modifiers *\/\n private const FILE_TYPE_MODIFIERS = [\n 'Controller' => [\n 'loc' => ['optimal' => 300, 'max' => 500],\n 'methods' => ['optimal' => 15, 'max' => 25],\n ],\n 'Entity' => [\n 'loc' => ['optimal' => 100, 'max' => 300],\n 'methods' => ['optimal' => 20, 'max' => 30],\n ],\n 'Repository' => [\n 'loc' => ['optimal' => 250, 'max' => 400],\n 'dependencies' => ['optimal' => 3, 'max' => 8],\n ],\n 'Service' => [\n 'loc' => ['optimal' => 150, 'max' => 350],\n ],\n 'UseCase' => [\n 'loc' => ['optimal' => 150, 'max' => 350],\n ],\n ];\n\n \/** @var array<string, float> Factor weights (must sum to 1.0) *\/\n private const WEIGHTS = [\n 'dependencies' => 0.25,\n 'loc' => 0.20,\n 'methods' => 0.20,\n 'secrets' => 0.15,\n 'classes' => 0.10,\n 'magic_numbers' => 0.10,\n ];\n\n \/** @var array<string, array{min: int, max: int}> Hygiene status ranges *\/\n private const HYGIENE_STATUS = [\n 'Sauber' => ['min' => 80, 'max' => 100],\n 'Gepflegt' => ['min' => 60, 'max' => 79],\n 'Vernachlässigt' => ['min' => 40, 'max' => 59],\n 'Verschmutzt' => ['min' => 20, 'max' => 39],\n 'Kritisch' => ['min' => 0, 'max' => 19],\n ];\n\n \/**\n * Analysiert eine Datei und berechnet den Code Hygiene Score.\n *\n * @param array<string, mixed> $analysisData Daten aus code_analysis\n * @return array{\n * hygiene_score: int,\n * hygiene_status: string,\n * factor_scores: array<string, int>,\n * issues_count: int,\n * warnings_count: int,\n * issues_json: string\n * }\n *\/\n public function analyze(array $analysisData): array\n {\n $issues = [];\n $filePath = $analysisData['file_path'] ?? '';\n $content = $analysisData['file_content'] ?? '';\n\n \/\/ Determine file type for modifiers\n $fileType = $this->detectFileType($filePath);\n $thresholds = $this->getThresholdsForType($fileType);\n\n \/\/ Extract metrics\n $metrics = [\n 'loc' => (int) ($analysisData['line_count'] ?? 0),\n 'methods' => $this->countMethods($analysisData),\n 'classes' => (int) ($analysisData['class_count'] ?? 1),\n 'dependencies' => $this->countDependencies($analysisData),\n 'secrets' => 0,\n 'magic_numbers' => 0,\n ];\n\n \/\/ Detect secrets and magic numbers\n $secretIssues = $this->detectSecrets($content, $filePath);\n $metrics['secrets'] = count($secretIssues);\n $issues = array_merge($issues, $secretIssues);\n\n $magicIssues = $this->detectMagicNumbers($content, $filePath);\n $metrics['magic_numbers'] = count($magicIssues);\n $issues = array_merge($issues, $magicIssues);\n\n \/\/ Add threshold violation issues\n $issues = array_merge($issues, $this->detectThresholdViolations($metrics, $thresholds));\n\n \/\/ Calculate normalized factor scores\n $factorScores = [];\n foreach (self::WEIGHTS as $factor => $weight) {\n $factorScores[$factor] = $this->normalize(\n $metrics[$factor],\n $thresholds[$factor]['optimal'],\n $thresholds[$factor]['max']\n );\n }\n\n \/\/ Calculate weighted hygiene score\n $hygieneScore = $this->calculateWeightedScore($factorScores);\n\n \/\/ Apply hard fail for secrets\n if ($metrics['secrets'] > 0) {\n $hygieneScore = min($hygieneScore, 20);\n }\n\n \/\/ Determine hygiene status\n $hygieneStatus = $this->determineStatus($hygieneScore);\n\n \/\/ Count warnings\n $warningsCount = count(array_filter($issues, fn ($i) => $i['severity'] === 'warning'));\n\n return [\n 'hygiene_score' => $hygieneScore,\n 'hygiene_status' => $hygieneStatus,\n 'factor_scores' => $factorScores,\n 'issues_count' => count($issues),\n 'warnings_count' => $warningsCount,\n 'issues_json' => json_encode($issues, JSON_UNESCAPED_UNICODE),\n ];\n }\n\n \/**\n * Normalisiert einen Wert auf 0-100 Skala.\n * Formel: max(0, 100 - ((value - optimal) \/ (max - optimal)) * 100)\n *\/\n private function normalize(int $value, int $optimal, int $max): int\n {\n if ($value <= $optimal) {\n return 100;\n }\n\n if ($value >= $max) {\n return 0;\n }\n\n $range = $max - $optimal;\n if ($range === 0) {\n return 0;\n }\n\n $normalized = 100 - (($value - $optimal) \/ $range) * 100;\n\n return (int) max(0, min(100, $normalized));\n }\n\n \/**\n * Berechnet den gewichteten Gesamtscore.\n *\n * @param array<string, int> $factorScores\n *\/\n private function calculateWeightedScore(array $factorScores): int\n {\n $weightedSum = 0.0;\n\n foreach (self::WEIGHTS as $factor => $weight) {\n $weightedSum += ($factorScores[$factor] ?? 0) * $weight;\n }\n\n return (int) round($weightedSum);\n }\n\n \/**\n * Ermittelt den Dateityp aus dem Pfad.\n *\/\n private function detectFileType(string $filePath): ?string\n {\n $filename = basename($filePath);\n\n foreach (array_keys(self::FILE_TYPE_MODIFIERS) as $type) {\n if (str_contains($filename, $type)) {\n return $type;\n }\n }\n\n \/\/ Check directory\n if (str_contains($filePath, '\/Controller\/')) {\n return 'Controller';\n }\n if (str_contains($filePath, '\/Entity\/')) {\n return 'Entity';\n }\n if (str_contains($filePath, '\/Repository\/') || str_contains($filePath, '\/Persistence\/')) {\n return 'Repository';\n }\n if (str_contains($filePath, '\/Service\/')) {\n return 'Service';\n }\n if (str_contains($filePath, '\/UseCase\/') || str_contains($filePath, '\/UseCases\/')) {\n return 'UseCase';\n }\n\n return null;\n }\n\n \/**\n * Gibt die Thresholds für einen Dateityp zurück.\n *\n * @return array<string, array{optimal: int, max: int}>\n *\/\n private function getThresholdsForType(?string $fileType): array\n {\n $thresholds = self::HYGIENE_DEFAULTS;\n\n if ($fileType !== null && isset(self::FILE_TYPE_MODIFIERS[$fileType])) {\n foreach (self::FILE_TYPE_MODIFIERS[$fileType] as $factor => $values) {\n $thresholds[$factor] = $values;\n }\n }\n\n return $thresholds;\n }\n\n \/**\n * Zählt die Methoden aus den Analysedaten.\n *\n * @param array<string, mixed> $analysisData\n *\/\n private function countMethods(array $analysisData): int\n {\n $functions = json_decode($analysisData['functions'] ?? '[]', true);\n\n return is_array($functions) ? count($functions) : 0;\n }\n\n \/**\n * Zählt die Dependencies aus den Analysedaten.\n *\n * @param array<string, mixed> $analysisData\n *\/\n private function countDependencies(array $analysisData): int\n {\n $uses = json_decode($analysisData['uses'] ?? '[]', true);\n\n return is_array($uses) ? count($uses) : 0;\n }\n\n \/**\n * Erkennt Secrets im Code (Hard Fail).\n *\n * @return array<array{type: string, rule: string, message: string, severity: string, line?: int}>\n *\/\n private function detectSecrets(string $content, string $filePath): array\n {\n $issues = [];\n $filename = basename($filePath);\n\n \/\/ Skip config files\n if (preg_match('\/^(config|\\.env|test|spec)\/i', $filename)) {\n return [];\n }\n\n foreach (self::SECRET_PATTERNS as $type => $pattern) {\n if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {\n foreach ($matches[0] as $match) {\n $line = substr_count(substr($content, 0, $match[1]), \"\\n\") + 1;\n $issues[] = [\n 'type' => 'secret',\n 'rule' => \"hardcoded-{$type}\",\n 'message' => \"KRITISCH: Mögliches hardcoded {$type} gefunden\",\n 'severity' => 'critical',\n 'line' => $line,\n ];\n }\n }\n }\n\n return $issues;\n }\n\n \/**\n * Erkennt Magic Numbers im Code.\n *\n * @return array<array{type: string, rule: string, message: str... [TRUNCATED-a42357cb98152efe]",
"structuredPatch": [
{
"oldStart": 4,
"oldLines": 210,
"newStart": 4,
"newLines": 324,
"lines": [
" ",
" namespace Infrastructure\\CodeAnalysis;",
" ",
"-\/\/ @responsibility: Code-Qualitätsanalyse (Hardcoded, Complexity, Metrics)",
"+\/\/ @responsibility: Code-Hygiene-Analyse (Score 0-100, normalisiert, gewichtet)",
" ",
" final class CodeQualityChecker",
" {",
"- \/** @var array<string, string> Regex patterns for hardcoded detection *\/",
"- private const HARDCODED_PATTERNS = [",
"+ \/** @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',",
"- 'ip_address' => '\/[\"\\']\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}[\"\\']\/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 int Minimum score for worst quality *\/",
"- private const SCORE_MIN = 20;",
"+ \/** @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, int> Thresholds for scoring *\/",
"- private const THRESHOLDS = [",
"- 'loc_excellent' => 100,",
"- 'loc_good' => 200,",
"- 'loc_acceptable' => 400,",
"- 'loc_poor' => 600,",
"- 'methods_excellent' => 5,",
"- 'methods_good' => 10,",
"- 'methods_acceptable' => 15,",
"- 'methods_poor' => 20,",
"- 'deps_excellent' => 3,",
"- 'deps_good' => 7,",
"- 'deps_acceptable' => 12,",
"- 'deps_poor' => 20,",
"+ \/** @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,",
"+ ];",
"+",
"+ \/** @var array<string, array{min: int, max: int}> Hygiene status ranges *\/",
"+ private const HYGIENE_STATUS = [",
"+ 'Sauber' => ['min' => 80, 'max' => 100],",
"+ 'Gepflegt' => ['min' => 60, 'max' => 79],",
"+ 'Vernachlässigt' => ['min' => 40, 'max' => 59],",
"+ 'Verschmutzt' => ['min' => 20, 'max' => 39],",
"+ 'Kritisch' => ['min' => 0, 'max' => 19],",
"+ ];",
"+",
" \/**",
"- * Analysiert eine Datei auf Qualitätsprobleme.",
"+ * Analysiert eine Datei und berechnet den Code Hygiene Score.",
" *",
" * @param array<string, mixed> $analysisData Daten aus code_analysis",
" * @return array{",
"- * complexity_score: int,",
"- * loc_score: int,",
"- * dependency_score: int,",
"- * hardcoded_count: int,",
"+ * hygiene_score: int,",
"+ * hygiene_status: string,",
"+ * factor_scores: array<string, int>,",
" * issues_count: int,",
" * warnings_count: int,",
"- * quality_grade: string,",
" * issues_json: string",
" * }",
" *\/",
" public function analyze(array $analysisData): array",
" {",
" $issues = [];",
"- $warnings = [];",
"-",
" $filePath = $analysisData['file_path'] ?? '';",
"- \/\/ Use stored content from DB (scan runs as root with full access)",
" $content = $analysisData['file_content'] ?? '';",
" ",
"- \/\/ LOC Analysis",
"- $lineCount = $analysisData['line_count'] ?? 0;",
"- $locScore = $this->calculateLocScore($lineCount);",
"- if ($lineCount > self::THRESHOLDS['loc_poor']) {",
"- $issues[] = [",
"- 'type' => 'complexity',",
"- 'rule' => 'file-too-long',",
"- 'message' => \"Datei hat {$lineCount} Zeilen (max empfohlen: \" . self::THRESHOLDS['loc_acceptable'] . ')',",
"- 'severity' => 'warning',",
"- ];",
"- }",
"+ \/\/ Determine file type for modifiers",
"+ $fileType = $this->detectFileType($filePath);",
"+ $thresholds = $this->getThresholdsForType($fileType);",
" ",
"- \/\/ Method Count Analysis",
"- $functions = json_decode($analysisData['functions'] ?? '[]', true);",
"- $methodCount = is_array($functions) ? count($functions) : 0;",
"- $methodScore = $this->calculateMethodScore($methodCount);",
"- if ($methodCount > self::THRESHOLDS['methods_poor']) {",
"- $issues[] = [",
"- 'type' => 'srp',",
"- 'rule' => 'too-many-methods',",
"- 'message' => \"Klasse hat {$methodCount} Methoden (SRP-Verletzung möglich)\",",
"- 'severity' => 'warning',",
"- ];",
"- }",
"+ \/\/ 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,",
"+ ];",
" ",
"- \/\/ Dependency Analysis",
"- $uses = json_decode($analysisData['uses'] ?? '[]', true);",
"- $depsCount = is_array($uses) ? count($uses) : 0;",
"- $depScore = $this->calculateDependencyScore($depsCount);",
"- if ($depsCount > self::THRESHOLDS['deps_poor']) {",
"- $issues[] = [",
"- 'type' => 'coupling',",
"- 'rule' => 'too-many-dependencies',",
"- 'message' => \"Klasse hat {$depsCount} Dependencies (hohe Kopplung)\",",
"- 'severity' => 'warning',",
"- ];",
"+ \/\/ 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']",
"+ );",
" }",
" ",
"- \/\/ Hardcoded Detection",
"- $hardcodedIssues = $this->detectHardcoded($content, $filePath);",
"- $hardcodedCount = count($hardcodedIssues);",
"- $issues = array_merge($issues, $hardcodedIssues);",
"+ \/\/ Calculate weighted hygiene score",
"+ $hygieneScore = $this->calculateWeightedScore($factorScores);",
" ",
"- \/\/ Calculate complexity score (weighted average)",
"- $complexityScore = (int) (($locScore * 0.3 + $methodScore * 0.3 + $depScore * 0.4));",
"+ \/\/ Apply hard fail for secrets",
"+ if ($metrics['secrets'] > 0) {",
"+ $hygieneScore = min($hygieneScore, 20);",
"+ }",
" ",
"- \/\/ Penalize for hardcoded values",
"- $complexityScore = max(0, $complexityScore - ($hardcodedCount * 10));",
"+ \/\/ Determine hygiene status",
"+ $hygieneStatus = $this->determineStatus($hygieneScore);",
" ",
"- \/\/ Determine grade",
"- $totalIssues = count($issues);",
"- $totalWarnings = count(array_filter($issues, fn ($i) => $i['severity'] === 'warning'));",
"- $grade = $this->calculateGrade($complexityScore, $totalIssues);",
"+ \/\/ Count warnings",
"+ $warningsCount = count(array_filter($issues, fn ($i) => $i['severity'] === 'warning'));",
" ",
" return [",
"- 'complexity_score' => $complexityScore,",
"- 'loc_score' => $locScore,",
"- 'dependency_score' => $depScore,",
"- 'hardcoded_count' => $hardcodedCount,",
"- 'issues_count' => $totalIssues,",
"- 'warnings_count' => $totalWarnings,",
"- 'quality_grade' => $grade,",
"+ 'hygiene_score' => $hygieneScore,",
"+ 'hygiene_status' => $hygieneStatus,",
"+ 'factor_scores' => $factorScores,",
"+ 'issues_count' => count($issues),",
"+ 'warnings_count' => $warningsCount,",
" 'issues_json' => json_encode($issues, JSON_UNESCAPED_UNICODE),",
" ];",
" }",
" ",
"- private function calculateLocScore(int $loc): int",
"+ \/**",
"+ * 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 ($loc <= self::THRESHOLDS['loc_excellent']) {",
"+ if ($value <= $optimal) {",
" return 100;",
" }",
"- if ($loc <= self::THRESHOLDS['loc_good']) {",
"- return 80;",
"+",
"+ if ($value >= $max) {",
"+ return 0;",
" }",
"- if ($loc <= self::THRESHOLDS['loc_acceptable']) {",
"- return 60;",
"+",
"+ $range = $max - $optimal;",
"+ if ($range === 0) {",
"+ return 0;",
" }",
"- if ($loc <= self::THRESHOLDS['loc_poor']) {",
"- return 40;",
"+",
"+ $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 self::SCORE_MIN;",
"+ return (int) round($weightedSum);",
" }",
" ",
"- private function calculateMethodScore(int $count): int",
"+ \/**",
"+ * Ermittelt den Dateityp aus dem Pfad.",
"+ *\/",
"+ private function detectFileType(string $filePath): ?string",
" {",
"- if ($count <= self::THRESHOLDS['methods_excellent']) {",
"- return 100;",
"+ $filename = basename($filePath);",
"+",
"+ foreach (array_keys(self::FILE_TYPE_MODIFIERS) as $type) {",
"+ if (str_contains($filename, $type)) {",
"+ return $type;",
"+ }",
" }",
"- if ($count <= self::THRESHOLDS['methods_good']) {",
"- return 80;",
"+",
"+ \/\/ Check directory",
"+ if (str_contains($filePath, '\/Controller\/')) {",
"+ return 'Controller';",
" }",
"- if ($count <= self::THRESHOLDS['methods_acceptable']) {",
"- return 60;",
"+ if (str_contains($filePath, '\/Entity\/')) {",
"+ return 'Entity';",
" }",
"- if ($count <= self::THRESHOLDS['methods_poor']) {",
"- return 40;",
"+ 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 self::SCORE_MIN;",
"+ return null;",
" }",
" ",
"- private function calculateDependencyScore(int $count): int",
"+ \/**",
"+ * Gibt die Thresholds für einen Dateityp zurück.",
"+ *",
"+ * @return array<string, array{optimal: int, max: int}>",
"+ *\/",
"+ private function getThresholdsForType(?string $fileType): array",
" {",
"- if ($count <= self::THRESHOLDS['deps_excellent']) {",
"- return 100;",
"+ $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;",
"+ }",
" }",
"- if ($count <= self::THRESHOLDS['deps_good']) {",
"- return 80;",
"- }",
"- if ($count <= self::THRESHOLDS['deps_acceptable']) {",
"- return 60;",
"- }",
"- if ($count <= self::THRESHOLDS['deps_poor']) {",
"- return 40;",
"- }",
" ",
"- return self::SCORE_MIN;",
"+ 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 detectHardcoded(string $content, string $filePath): array",
"+ private function detectSecrets(string $content, string $filePath): array",
" {",
" $issues = [];",
"-",
"- \/\/ Skip config files and test files",
" $filename = basename($filePath);",
"+",
"+ \/\/ Skip config files",
" if (preg_match('\/^(config|\\.env|test|spec)\/i', $filename)) {",
" return [];",
" }",
" ",
"- foreach (self::HARDCODED_PATTERNS as $type => $pattern) {",
"- \/\/ Skip magic numbers in config\/constant files",
"- if ($type === 'magic_number' && preg_match('\/const|config\/i', $filePath)) {",
"- continue;",
"+ 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' => 'hardcoded',",
"- 'rule' => \"hardcoded-{$type}\",",
"- 'message' => \"Möglicher hardcoded {$type}: \" . substr($match[0], 0, 30) . '...',",
"- 'severity' => $type === 'magic_number' ? 'info' : 'warning',",
"+ 'type' => 'magic_number',",
"+ 'rule' => 'hardcoded-magic-number',",
"+ 'message' => \"Magic Number gefunden: {$match[0]}\",",
"+ 'severity' => 'info',",
" 'line' => $line,",
" ];",
" }"
]
},
{
"oldStart": 217,
"oldLines": 24,
"newStart": 331,
"newLines": 58,
"lines": [
" return $issues;",
" }",
" ",
"- private function calculateGrade(int $score, int $issueCount): string",
"+ \/**",
"+ * 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",
" {",
"- \/\/ Penalize for issues",
"- $adjustedScore = $score - ($issueCount * 5);",
"+ $issues = [];",
" ",
"- if ($adjustedScore >= 90) {",
"- return 'A';",
"+ 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 ($adjustedScore >= 75) {",
"- return 'B';",
"+",
"+ 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 ($adjustedScore >= 60) {",
"- return 'C';",
"+",
"+ 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',",
"+ ];",
" }",
"- if ($adjustedScore >= 40) {",
"- return 'D';",
"+",
"+ return $issues;",
"+ }",
"+",
"+ \/**",
"+ * Bestimmt den Hygiene-Status basierend auf dem Score.",
"+ *\/",
"+ private function determineStatus(int $score): string",
"+ {",
"+ foreach (self::HYGIENE_STATUS as $status => $range) {",
"+ if ($score >= $range['min'] && $score <= $range['max']) {",
"+ return $status;",
"+ }",
" }",
" ",
"- return 'F';",
"+ return 'Kritisch';",
" }",
" }"
]
}
],
"originalFile": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Infrastructure\\CodeAnalysis;\n\n\/\/ @responsibility: Code-Qualitätsanalyse (Hardcoded, Complexity, Metrics)\n\nfinal class CodeQualityChecker\n{\n \/** @var array<string, string> Regex patterns for hardcoded detection *\/\n private const HARDCODED_PATTERNS = [\n 'password' => '\/[\"\\'](?:password|passwd|pwd)[\"\\']\\\\s*[=:]\\\\s*[\"\\'][^\"\\']{3,}[\"\\']\/i',\n 'api_key' => '\/[\"\\'](?:api[_-]?key|apikey|secret[_-]?key)[\"\\']\\\\s*[=:]\\\\s*[\"\\'][^\"\\']{8,}[\"\\']\/i',\n 'token' => '\/[\"\\'](?:token|auth[_-]?token|access[_-]?token)[\"\\']\\\\s*[=:]\\\\s*[\"\\'][^\"\\']{8,}[\"\\']\/i',\n 'ip_address' => '\/[\"\\']\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}[\"\\']\/i',\n 'url_with_creds' => '\/https?:\\\\\/\\\\\/[^:]+:[^@]+@\/i',\n 'magic_number' => '\/(?<![\\\\w])(?:100|1000|60|24|365|3600|86400)(?![\\\\w])\/',\n ];\n\n \/** @var int Minimum score for worst quality *\/\n private const SCORE_MIN = 20;\n\n \/** @var array<string, int> Thresholds for scoring *\/\n private const THRESHOLDS = [\n 'loc_excellent' => 100,\n 'loc_good' => 200,\n 'loc_acceptable' => 400,\n 'loc_poor' => 600,\n 'methods_excellent' => 5,\n 'methods_good' => 10,\n 'methods_acceptable' => 15,\n 'methods_poor' => 20,\n 'deps_excellent' => 3,\n 'deps_good' => 7,\n 'deps_acceptable' => 12,\n 'deps_poor' => 20,\n ];\n\n \/**\n * Analysiert eine Datei auf Qualitätsprobleme.\n *\n * @param array<string, mixed> $analysisData Daten aus code_analysis\n * @return array{\n * complexity_score: int,\n * loc_score: int,\n * dependency_score: int,\n * hardcoded_count: int,\n * issues_count: int,\n * warnings_count: int,\n * quality_grade: string,\n * issues_json: string\n * }\n *\/\n public function analyze(array $analysisData): array\n {\n $issues = [];\n $warnings = [];\n\n $filePath = $analysisData['file_path'] ?? '';\n \/\/ Use stored content from DB (scan runs as root with full access)\n $content = $analysisData['file_content'] ?? '';\n\n \/\/ LOC Analysis\n $lineCount = $analysisData['line_count'] ?? 0;\n $locScore = $this->calculateLocScore($lineCount);\n if ($lineCount > self::THRESHOLDS['loc_poor']) {\n $issues[] = [\n 'type' => 'complexity',\n 'rule' => 'file-too-long',\n 'message' => \"Datei hat {$lineCount} Zeilen (max empfohlen: \" . self::THRESHOLDS['loc_acceptable'] . ')',\n 'severity' => 'warning',\n ];\n }\n\n \/\/ Method Count Analysis\n $functions = json_decode($analysisData['functions'] ?? '[]', true);\n $methodCount = is_array($functions) ? count($functions) : 0;\n $methodScore = $this->calculateMethodScore($methodCount);\n if ($methodCount > self::THRESHOLDS['methods_poor']) {\n $issues[] = [\n 'type' => 'srp',\n 'rule' => 'too-many-methods',\n 'message' => \"Klasse hat {$methodCount} Methoden (SRP-Verletzung möglich)\",\n 'severity' => 'warning',\n ];\n }\n\n \/\/ Dependency Analysis\n $uses = json_decode($analysisData['uses'] ?? '[]', true);\n $depsCount = is_array($uses) ? count($uses) : 0;\n $depScore = $this->calculateDependencyScore($depsCount);\n if ($depsCount > self::THRESHOLDS['deps_poor']) {\n $issues[] = [\n 'type' => 'coupling',\n 'rule' => 'too-many-dependencies',\n 'message' => \"Klasse hat {$depsCount} Dependencies (hohe Kopplung)\",\n 'severity' => 'warning',\n ];\n }\n\n \/\/ Hardcoded Detection\n $hardcodedIssues = $this->detectHardcoded($content, $filePath);\n $hardcodedCount = count($hardcodedIssues);\n $issues = array_merge($issues, $hardcodedIssues);\n\n \/\/ Calculate complexity score (weighted average)\n $complexityScore = (int) (($locScore * 0.3 + $methodScore * 0.3 + $depScore * 0.4));\n\n \/\/ Penalize for hardcoded values\n $complexityScore = max(0, $complexityScore - ($hardcodedCount * 10));\n\n \/\/ Determine grade\n $totalIssues = count($issues);\n $totalWarnings = count(array_filter($issues, fn ($i) => $i['severity'] === 'warning'));\n $grade = $this->calculateGrade($complexityScore, $totalIssues);\n\n return [\n 'complexity_score' => $complexityScore,\n 'loc_score' => $locScore,\n 'dependency_score' => $depScore,\n 'hardcoded_count' => $hardcodedCount,\n 'issues_count' => $totalIssues,\n 'warnings_count' => $totalWarnings,\n 'quality_grade' => $grade,\n 'issues_json' => json_encode($issues, JSON_UNESCAPED_UNICODE),\n ];\n }\n\n private function calculateLocScore(int $loc): int\n {\n if ($loc <= self::THRESHOLDS['loc_excellent']) {\n return 100;\n }\n if ($loc <= self::THRESHOLDS['loc_good']) {\n return 80;\n }\n if ($loc <= self::THRESHOLDS['loc_acceptable']) {\n return 60;\n }\n if ($loc <= self::THRESHOLDS['loc_poor']) {\n return 40;\n }\n\n return self::SCORE_MIN;\n }\n\n private function calculateMethodScore(int $count): int\n {\n if ($count <= self::THRESHOLDS['methods_excellent']) {\n return 100;\n }\n if ($count <= self::THRESHOLDS['methods_good']) {\n return 80;\n }\n if ($count <= self::THRESHOLDS['methods_acceptable']) {\n return 60;\n }\n if ($count <= self::THRESHOLDS['methods_poor']) {\n return 40;\n }\n\n return self::SCORE_MIN;\n }\n\n private function calculateDependencyScore(int $count): int\n {\n if ($count <= self::THRESHOLDS['deps_excellent']) {\n return 100;\n }\n if ($count <= self::THRESHOLDS['deps_good']) {\n return 80;\n }\n if ($count <= self::THRESHOLDS['deps_acceptable']) {\n return 60;\n }\n if ($count <= self::THRESHOLDS['deps_poor']) {\n return 40;\n }\n\n return self::SCORE_MIN;\n }\n\n \/**\n * @return array<array{type: string, rule: string, message: string, severity: string, line?: int}>\n *\/\n private function detectHardcoded(string $content, string $filePath): array\n {\n $issues = [];\n\n \/\/ Skip config files and test files\n $filename = basename($filePath);\n if (preg_match('\/^(config|\\.env|test|spec)\/i', $filename)) {\n return [];\n }\n\n foreach (self::HARDCODED_PATTERNS as $type => $pattern) {\n \/\/ Skip magic numbers in config\/constant files\n if ($type === 'magic_number' && preg_match('\/const|config\/i', $filePath)) {\n continue;\n }\n\n if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {\n foreach ($matches[0] as $match) {\n $line = substr_count(substr($content, 0, $match[1]), \"\\n\") + 1;\n $issues[] = [\n 'type' => 'hardcoded',\n 'rule' => \"hardcoded-{$type}\",\n 'message' => \"Möglicher hardcoded {$type}: \" . substr($match[0], 0, 30) . '...',\n 'severity' => $type === 'magic_number' ? 'info' : 'warning',\n 'line' => $line,\n ];\n }\n }\n }\n\n return $issues;\n }\n\n private function calculateGrade(int $score, int $issueCount): string\n {\n \/\/ Penalize for issues\n $adjustedScore = $score - ($issueCount * 5);\n\n if ($adjustedScore >= 90) {\n return 'A';\n }\n if ($adjustedScore >= 75) {\n return 'B';\n }\n if ($adjustedScore >= 60) {\n return 'C';\n }\n if ($adjustedScore >= 40) {\n return 'D';\n }\n\n return 'F';\n }\n}\n"
}
}