Backup #1805
| ID | 1805 |
| Dateipfad | /var/www/dev.campus.systemische-tools.de/src/Infrastructure/CodeAnalysis/CodeQualityChecker.php |
| Version | 10 |
| Typ |
modified |
| Größe | 11.6 KB |
| Hash | 7d7345caf41a395a2089ee8a3607ba62b4ab08dd03feb9615c44e6d0b19030ea |
| Datum | 2025-12-27 15:28:23 |
| Geändert von | claude-code-hook |
| Grund | Claude 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?:\\/\\/[^\\s:\\/]+:[^\\s@]+@/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
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