import * as fs from 'node:fs'; import * as path from 'node:path'; import type { DependencyGraph, FileContext } from '../types/analysis.js'; const LANGUAGE_MAP: Record = { '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.go': 'go', '.rs': 'rust', '.java': 'java', '.rb': 'ruby', '.php': 'php', '.c': 'c', '.cpp': 'cpp', '.cs': 'csharp', '.swift': 'swift', '.kt': 'kotlin', }; const TEST_PATTERNS = [ /\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /_test\.go$/, /test_.*\.py$/, /.*_test\.py$/, /\.test\.py$/, /Test\.java$/, /\.test\.rb$/, ]; const CONFIG_PATTERNS = [ /\.config\.[jt]s$/, /\.rc$/, /\.json$/, /\.ya?ml$/, /\.toml$/, /\.ini$/, /\.env/, /Makefile$/, /Dockerfile$/, ]; const GENERATED_MARKERS = [ '// Code generated', '# Generated by', '// AUTO-GENERATED', '/* eslint-disable */', '// @generated', ]; export function buildFileContext( filePath: string, projectRoot: string, graph?: DependencyGraph, ): FileContext { const content = fs.readFileSync(filePath, 'utf-8'); const ext = path.extname(filePath); const language = LANGUAGE_MAP[ext] ?? 'unknown'; const lines = content.split('\n'); const relativePath = path.relative(projectRoot, filePath); const dirName = path.dirname(filePath); let siblingFiles: string[] = []; try { siblingFiles = fs .readdirSync(dirName) .filter((f) => { const full = path.join(dirName, f); try { return fs.statSync(full).isFile(); } catch { return false; } }) .filter((f) => f !== path.basename(filePath)) .slice(0, 20); } catch { /* dir read error */ } const imports = extractImports(content, language); let importedBy: string[] = []; if (graph) { const node = graph.nodes.get(relativePath) ?? graph.nodes.get(filePath); if (node) { importedBy = node.importedBy; } } return { filePath: relativePath, content, language, lineCount: lines.length, imports, importedBy, siblingFiles, isTestFile: isTestFile(relativePath), isConfigFile: isConfigFile(relativePath), isGenerated: isGeneratedFile(content), }; } export function isTestFile(filePath: string): boolean { const name = path.basename(filePath); // Normalize separators for cross-platform matching const normalized = filePath.replace(/\\/g, '/'); return TEST_PATTERNS.some((p) => p.test(name)) || /(^|\/)(test|tests|__tests__)\//.test(normalized); } export function isConfigFile(filePath: string): boolean { const name = path.basename(filePath); return CONFIG_PATTERNS.some((p) => p.test(name)); } export function isGeneratedFile(content: string): boolean { const header = content.slice(0, 500); return GENERATED_MARKERS.some((m) => header.includes(m)); } function extractImports(content: string, language: string): string[] { const imports: string[] = []; if (['javascript', 'typescript'].includes(language)) { // ES imports const esImports = content.matchAll(/import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g); for (const m of esImports) imports.push(m[1]); // require const requires = content.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g); for (const m of requires) imports.push(m[1]); } else if (language === 'python') { const pyImports = content.matchAll(/(?:from\s+(\S+)\s+import|import\s+(\S+))/g); for (const m of pyImports) imports.push(m[1] ?? m[2]); } else if (language === 'go') { const goImports = content.matchAll(/import\s+(?:\(\s*)?["']([^"']+)["']/g); for (const m of goImports) imports.push(m[1]); } else if (language === 'java') { const javaImports = content.matchAll(/import\s+([\w.]+);/g); for (const m of javaImports) imports.push(m[1]); } return [...new Set(imports)]; }