CodeScanner.php

Code Hygiene Score: 91

Keine Issues gefunden.

Dependencies 10

Klassen 1

Funktionen 12

Verwendet von 1

Versionen 20

Code

<?php

declare(strict_types=1);

namespace Infrastructure\CodeAnalysis;

// @responsibility: Rekursives Verzeichnis-Scanning für Code-Analyse

use Domain\Repository\CodeQualityRepositoryInterface;
use Domain\Repository\FileAnalysisRepositoryInterface;
use Domain\Service\CodeScannerInterface;

final class CodeScanner implements CodeScannerInterface
{
    public function __construct(
        private FileAnalysisRepositoryInterface $fileRepository,
        private CodeQualityRepositoryInterface $qualityRepository,
        private PhpFileParser $phpParser,
        private PythonFileParser $pythonParser,
        private JsFileParser $jsParser,
        private CodeQualityChecker $qualityChecker
    ) {
    }

    /**
     * Scannt konfigurierte oder übergebene Verzeichnisse.
     *
     * @param array<string>|null $directories Null = konfigurierte Verzeichnisse
     * @return array{scan_id: string, files_scanned: int, files_with_errors: int, total_classes: int, total_functions: int, duration_ms: int}
     */
    public function scan(?array $directories = null, string $triggeredBy = 'web'): array
    {
        $start = hrtime(true);
        $scanId = $this->generateUuid();

        $dirs = $directories ?? $this->getEnabledDirectories();
        $items = [];
        $totalClasses = 0;
        $totalFunctions = 0;
        $filesWithErrors = 0;

        foreach ($dirs as $directory) {
            $config = $this->getConfigForDirectory($directory);
            $extensions = $this->parseExtensions($config['extensions'] ?? 'php');
            $excludePatterns = $this->parseExcludePatterns($config['exclude_patterns'] ?? '');

            $files = $this->scanDirectory($directory, $extensions, $excludePatterns);

            foreach ($files as $filePath) {
                $item = $this->analyzeFile($filePath, $triggeredBy);
                $items[] = $item;

                if ($item['parse_error'] !== null) {
                    $filesWithErrors++;
                }
                $totalClasses += count($item['classes']);
                $totalFunctions += count($item['functions']);
            }
        }

        $this->fileRepository->saveBatch($items, $scanId);
        $this->fileRepository->deleteByNotScanId($scanId);

        // Quality-Analyse ausführen
        $qualityStats = $this->runQualityAnalysis($scanId);

        $durationMs = (int) ((hrtime(true) - $start) / 1_000_000);

        return [
            'scan_id' => $scanId,
            'files_scanned' => count($items),
            'files_with_errors' => $filesWithErrors,
            'total_classes' => $totalClasses,
            'total_functions' => $totalFunctions,
            'duration_ms' => $durationMs,
            'quality_issues' => $qualityStats['total_issues'],
            'avg_hygiene_score' => $qualityStats['avg_hygiene_score'],
        ];
    }

    /**
     * @return array<string>
     */
    private function getEnabledDirectories(): array
    {
        $configs = $this->fileRepository->getConfiguredDirectories();

        return array_column($configs, 'directory');
    }

    /**
     * @return array<string, mixed>
     */
    private function getConfigForDirectory(string $directory): array
    {
        $configs = $this->fileRepository->getConfiguredDirectories();
        foreach ($configs as $config) {
            if ($config['directory'] === $directory) {
                return $config;
            }
        }

        return ['extensions' => 'php', 'exclude_patterns' => ''];
    }

    /**
     * @return array<string>
     */
    private function parseExtensions(string $extensions): array
    {
        return array_map('trim', explode(',', $extensions));
    }

    /**
     * @return array<string>
     */
    private function parseExcludePatterns(string $patterns): array
    {
        if ($patterns === '') {
            return [];
        }

        return array_map('trim', explode(',', $patterns));
    }

    /**
     * @param array<string> $extensions
     * @param array<string> $excludePatterns
     * @return array<string>
     */
    private function scanDirectory(string $directory, array $extensions, array $excludePatterns): array
    {
        $files = [];

        if (!is_dir($directory)) {
            return $files;
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS)
        );

        foreach ($iterator as $file) {
            if (!$file->isFile()) {
                continue;
            }

            $filePath = $file->getPathname();
            $ext = $file->getExtension();

            if (!in_array($ext, $extensions, true)) {
                continue;
            }

            if ($this->isExcluded($filePath, $directory, $excludePatterns)) {
                continue;
            }

            $files[] = $filePath;
        }

        return $files;
    }

    /**
     * @param array<string> $patterns
     */
    private function isExcluded(string $filePath, string $rootDir, array $patterns): bool
    {
        $relativePath = str_replace($rootDir . '/', '', $filePath);

        foreach ($patterns as $pattern) {
            if (fnmatch($pattern, $relativePath)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return array<string, mixed>
     */
    private function analyzeFile(string $filePath, string $triggeredBy): array
    {
        $extension = pathinfo($filePath, PATHINFO_EXTENSION);
        $parsed = $this->getParserForExtension($extension)->parse($filePath);
        $stat = @stat($filePath);
        $fileContent = @file_get_contents($filePath) ?: '';
        $lineCount = $fileContent !== '' ? substr_count($fileContent, "\n") + 1 : 0;

        return [
            'file_path' => $filePath,
            'file_name' => basename($filePath),
            'extension' => pathinfo($filePath, PATHINFO_EXTENSION),
            'directory' => dirname($filePath),
            'file_size' => $stat ? $stat['size'] : 0,
            'line_count' => $lineCount,
            'file_content' => $fileContent,
            'modified_at' => $stat ? date('Y-m-d H:i:s', $stat['mtime']) : date('Y-m-d H:i:s'),
            'namespace' => $parsed['namespace'],
            'classes' => $parsed['classes'],
            'functions' => $parsed['functions'],
            'uses' => $parsed['uses'],
            'extends_class' => $parsed['extends_class'],
            'implements_interfaces' => $parsed['implements_interfaces'],
            'traits_used' => $parsed['traits_used'],
            'constructor_deps' => $parsed['constructor_deps'],
            'parse_error' => $parsed['error'],
            'triggered_by' => $triggeredBy,
        ];
    }

    /**
     * Wählt den passenden Parser für die Dateiendung.
     */
    private function getParserForExtension(string $extension): PhpFileParser|PythonFileParser|JsFileParser
    {
        return match ($extension) {
            'py' => $this->pythonParser,
            'js' => $this->jsParser,
            default => $this->phpParser,
        };
    }

    private function generateUuid(): string
    {
        return sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            random_int(0, 0xffff),
            random_int(0, 0xffff),
            random_int(0, 0xffff),
            random_int(0, 0x0fff) | 0x4000,
            random_int(0, 0x3fff) | 0x8000,
            random_int(0, 0xffff),
            random_int(0, 0xffff),
            random_int(0, 0xffff)
        );
    }

    /**
     * Führt Hygiene-Analyse für alle Dateien des Scans aus.
     *
     * @return array{total_issues: int, avg_hygiene_score: int}
     */
    private function runQualityAnalysis(string $scanId): array
    {
        $files = $this->fileRepository->findByScanId($scanId);
        $totalIssues = 0;
        $hygieneScores = [];

        foreach ($files as $file) {
            $qualityResult = $this->qualityChecker->analyze($file);
            $this->qualityRepository->saveQuality((int) $file['id'], $qualityResult);

            $totalIssues += $qualityResult['issues_count'];
            $hygieneScores[] = $qualityResult['hygiene_score'];
        }

        $avgScore = count($hygieneScores) > 0
            ? (int) round(array_sum($hygieneScores) / count($hygieneScores))
            : 50;

        return [
            'total_issues' => $totalIssues,
            'avg_hygiene_score' => $avgScore,
        ];
    }
}
← Übersicht Graph