diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index 54b5ea070..301f6d7fd 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -101,6 +101,37 @@ async function loadGoModulePath(repoRoot: string): Promise directory (e.g., "App\\" -> "app/") */ + psr4: Map; +} + +async function loadComposerConfig(repoRoot: string): Promise { + try { + const composerPath = path.join(repoRoot, 'composer.json'); + const raw = await fs.readFile(composerPath, 'utf-8'); + const composer = JSON.parse(raw); + const psr4Raw = composer.autoload?.['psr-4'] ?? {}; + const psr4Dev = composer['autoload-dev']?.['psr-4'] ?? {}; + const merged = { ...psr4Raw, ...psr4Dev }; + + const psr4 = new Map(); + for (const [ns, dir] of Object.entries(merged)) { + const nsNorm = (ns as string).replace(/\\+$/, ''); + const dirNorm = (dir as string).replace(/\\/g, '/').replace(/\/+$/, ''); + psr4.set(nsNorm, dirNorm); + } + + if (isDev) { + console.log(`📦 Loaded ${psr4.size} PSR-4 mappings from composer.json`); + } + return { psr4 }; + } catch { + return null; + } +} + // ============================================================================ // IMPORT PATH RESOLUTION // ============================================================================ @@ -122,6 +153,8 @@ const EXTENSIONS = [ '.go', // Rust '.rs', '/mod.rs', + // PHP + '.php', '.phtml', ]; /** @@ -551,6 +584,48 @@ function resolveGoPackage( return matches; } +// ============================================================================ +// PHP PSR-4 IMPORT RESOLUTION +// ============================================================================ + +/** + * Resolve a PHP use-statement import path using PSR-4 mappings. + * e.g. "App\Http\Controllers\UserController" -> "app/Http/Controllers/UserController.php" + */ +function resolvePhpImport( + importPath: string, + composerConfig: ComposerConfig | null, + allFiles: Set, + normalizedFileList: string[], + allFileList: string[], + index?: SuffixIndex, +): string | null { + // Normalize: replace backslashes with forward slashes + const normalized = importPath.replace(/\\/g, '/'); + + // Try PSR-4 resolution if composer.json was found + if (composerConfig) { + // Sort namespaces by length descending (longest match wins) + const sorted = [...composerConfig.psr4.entries()].sort((a, b) => b[0].length - a[0].length); + for (const [nsPrefix, dirPrefix] of sorted) { + const nsPrefixSlash = nsPrefix.replace(/\\/g, '/'); + if (normalized.startsWith(nsPrefixSlash + '/') || normalized === nsPrefixSlash) { + const remainder = normalized.slice(nsPrefixSlash.length).replace(/^\//, ''); + const filePath = dirPrefix + (remainder ? '/' + remainder : '') + '.php'; + if (allFiles.has(filePath)) return filePath; + if (index) { + const result = index.getInsensitive(filePath); + if (result) return result; + } + } + } + } + + // Fallback: suffix matching (works without composer.json) + const pathParts = normalized.split('/').filter(Boolean); + return suffixResolve(pathParts, normalizedFileList, allFileList, index); +} + // ============================================================================ // MAIN IMPORT PROCESSOR // ============================================================================ @@ -581,6 +656,7 @@ export const processImports = async ( const effectiveRoot = repoRoot || ''; const tsconfigPaths = await loadTsconfigPaths(effectiveRoot); const goModule = await loadGoModulePath(effectiveRoot); + const composerConfig = await loadComposerConfig(effectiveRoot); // Helper: add an IMPORTS edge + update import map const addImportEdge = (filePath: string, resolvedPath: string) => { @@ -705,6 +781,15 @@ export const processImports = async ( // Fall through if no files found (package might be external) } + // ---- PHP: handle namespace-based imports (use statements) ---- + if (language === SupportedLanguages.PHP) { + const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index); + if (resolved) { + addImportEdge(file.path, resolved); + } + return; + } + // ---- Standard single-file resolution ---- const resolvedPath = resolveImportPath( file.path, @@ -757,6 +842,7 @@ export const processImportsFromExtracted = async ( const effectiveRoot = repoRoot || ''; const tsconfigPaths = await loadTsconfigPaths(effectiveRoot); const goModule = await loadGoModulePath(effectiveRoot); + const composerConfig = await loadComposerConfig(effectiveRoot); const addImportEdge = (filePath: string, resolvedPath: string) => { const sourceId = generateId('File', filePath); @@ -856,6 +942,16 @@ export const processImportsFromExtracted = async ( } } + // PHP: handle namespace-based imports (use statements) + if (language === SupportedLanguages.PHP) { + const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index); + if (resolved) { + resolveCache.set(cacheKey, resolved); + addImportEdge(filePath, resolved); + } + continue; + } + // Standard resolution (has its own internal cache) const resolvedPath = resolveImportPath( filePath,