, * functions: array, * uses: array, * extends_class: string|null, * implements_interfaces: array, * traits_used: array, * constructor_deps: array, * error: string|null * } */ public function parse(string $filePath): array { $result = [ 'namespace' => null, 'classes' => [], 'functions' => [], 'uses' => [], 'extends_class' => null, 'implements_interfaces' => [], 'traits_used' => [], 'constructor_deps' => [], 'error' => null, ]; if (!file_exists($filePath) || !is_readable($filePath)) { $result['error'] = 'Datei nicht lesbar'; return $result; } // Python-Script ausführen $command = sprintf( 'python3 %s %s 2>&1', escapeshellarg(self::SCRIPT_PATH), escapeshellarg($filePath) ); $output = shell_exec($command); if ($output === null) { $result['error'] = 'Python-Parser konnte nicht ausgeführt werden'; return $result; } $parsed = json_decode($output, true); if ($parsed === null) { $result['error'] = 'JSON-Parsing fehlgeschlagen: ' . $output; return $result; } if (isset($parsed['error']) && $parsed['error'] !== null) { $result['error'] = $parsed['error']; return $result; } // Namespace aus Verzeichnisstruktur ableiten $result['namespace'] = $this->extractNamespace($filePath); // Klassen konvertieren foreach ($parsed['classes'] ?? [] as $class) { $result['classes'][] = [ 'name' => $class['name'], 'type' => 'class', 'line' => $class['line'], ]; // Erste Basisklasse als extends_class if (!empty($class['bases']) && $result['extends_class'] === null) { $result['extends_class'] = $class['bases'][0]; } // Weitere Basisklassen als "Mixins" (ähnlich Traits) if (count($class['bases'] ?? []) > 1) { $result['traits_used'] = array_merge( $result['traits_used'], array_slice($class['bases'], 1) ); } } // Funktionen konvertieren foreach ($parsed['functions'] ?? [] as $func) { $result['functions'][] = [ 'name' => $func['name'], 'visibility' => null, 'line' => $func['line'], ]; } // Imports als uses foreach ($parsed['imports'] ?? [] as $import) { if ($import['type'] === 'import') { $result['uses'][] = $import['module']; } else { // from X import Y -> X.Y $result['uses'][] = $import['full'] ?? $import['module']; } } $result['uses'] = array_unique($result['uses']); return $result; } /** * Extrahiert einen pseudo-Namespace aus dem Dateipfad. */ private function extractNamespace(string $filePath): ?string { // /opt/mcp-servers/mcp-db/tools/select_tool.py -> mcp-db.tools if (preg_match('#/opt/mcp-servers/([^/]+)/(.+)\.py$#', $filePath, $matches)) { $server = $matches[1]; $subPath = dirname($matches[2]); if ($subPath === '.') { return $server; } return $server . '.' . str_replace('/', '.', $subPath); } // /var/www/scripts/pipeline/detect.py -> pipeline if (preg_match('#/var/www/scripts/([^/]+)#', $filePath, $matches)) { return $matches[1]; } // /var/www/tools/ki-protokoll/claude-hook/quality/pre_rules.py -> claude-hook.quality if (preg_match('#/claude-hook/(.+)\.py$#', $filePath, $matches)) { $subPath = dirname($matches[1]); if ($subPath === '.') { return 'claude-hook'; } return 'claude-hook.' . str_replace('/', '.', $subPath); } return null; } }