| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- /**
- * Import Resolver
- *
- * Resolves import paths to actual files and symbols.
- */
- import * as path from 'path';
- import { Language, Node } from '../types';
- import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping } from './types';
- /**
- * Extension resolution order by language
- */
- const EXTENSION_RESOLUTION: Record<string, string[]> = {
- typescript: ['.ts', '.tsx', '.d.ts', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'],
- javascript: ['.js', '.jsx', '.mjs', '.cjs', '/index.js', '/index.jsx'],
- tsx: ['.tsx', '.ts', '.d.ts', '.js', '.jsx', '/index.tsx', '/index.ts', '/index.js'],
- jsx: ['.jsx', '.js', '/index.jsx', '/index.js'],
- python: ['.py', '/__init__.py'],
- go: ['.go'],
- rust: ['.rs', '/mod.rs'],
- java: ['.java'],
- csharp: ['.cs'],
- php: ['.php'],
- ruby: ['.rb'],
- };
- /**
- * Resolve an import path to an actual file
- */
- export function resolveImportPath(
- importPath: string,
- fromFile: string,
- language: Language,
- context: ResolutionContext
- ): string | null {
- // Skip external/npm packages
- if (isExternalImport(importPath, language)) {
- return null;
- }
- const projectRoot = context.getProjectRoot();
- const fromDir = path.dirname(path.join(projectRoot, fromFile));
- // Handle relative imports
- if (importPath.startsWith('.')) {
- return resolveRelativeImport(importPath, fromDir, language, context);
- }
- // Handle absolute/aliased imports (like @/ or src/)
- return resolveAliasedImport(importPath, projectRoot, language, context);
- }
- /**
- * Check if an import is external (npm package, etc.)
- */
- function isExternalImport(importPath: string, language: Language): boolean {
- // Relative imports are not external
- if (importPath.startsWith('.')) {
- return false;
- }
- // Common external patterns
- if (language === 'typescript' || language === 'javascript' || language === 'tsx' || language === 'jsx') {
- // Node built-ins
- if (['fs', 'path', 'os', 'crypto', 'http', 'https', 'url', 'util', 'events', 'stream', 'child_process', 'buffer'].includes(importPath)) {
- return true;
- }
- // Scoped packages or bare specifiers that don't start with aliases
- if (!importPath.startsWith('@/') && !importPath.startsWith('~/') && !importPath.startsWith('src/')) {
- // Likely an npm package
- return true;
- }
- }
- if (language === 'python') {
- // Standard library modules
- const stdLibs = ['os', 'sys', 'json', 're', 'math', 'datetime', 'collections', 'typing', 'pathlib', 'logging'];
- if (stdLibs.includes(importPath.split('.')[0]!)) {
- return true;
- }
- }
- if (language === 'go') {
- // Standard library or external packages
- if (!importPath.startsWith('.') && !importPath.includes('/internal/')) {
- return true;
- }
- }
- return false;
- }
- /**
- * Resolve a relative import
- */
- function resolveRelativeImport(
- importPath: string,
- fromDir: string,
- language: Language,
- context: ResolutionContext
- ): string | null {
- const projectRoot = context.getProjectRoot();
- const extensions = EXTENSION_RESOLUTION[language] || [];
- // Try the path as-is first
- const basePath = path.resolve(fromDir, importPath);
- const relativePath = path.relative(projectRoot, basePath);
- // Try each extension
- for (const ext of extensions) {
- const candidatePath = relativePath + ext;
- if (context.fileExists(candidatePath)) {
- return candidatePath;
- }
- }
- // Try without extension (might already have one)
- if (context.fileExists(relativePath)) {
- return relativePath;
- }
- return null;
- }
- /**
- * Resolve an aliased/absolute import
- */
- function resolveAliasedImport(
- importPath: string,
- _projectRoot: string,
- language: Language,
- context: ResolutionContext
- ): string | null {
- const extensions = EXTENSION_RESOLUTION[language] || [];
- // Common aliases
- const aliases: Record<string, string> = {
- '@/': 'src/',
- '~/': 'src/',
- '@src/': 'src/',
- 'src/': 'src/',
- '@app/': 'app/',
- 'app/': 'app/',
- };
- // Try each alias
- for (const [alias, replacement] of Object.entries(aliases)) {
- if (importPath.startsWith(alias)) {
- const resolvedPath = importPath.replace(alias, replacement);
- // Try with extensions
- for (const ext of extensions) {
- const candidatePath = resolvedPath + ext;
- if (context.fileExists(candidatePath)) {
- return candidatePath;
- }
- }
- // Try as-is
- if (context.fileExists(resolvedPath)) {
- return resolvedPath;
- }
- }
- }
- // Try direct path
- for (const ext of extensions) {
- const candidatePath = importPath + ext;
- if (context.fileExists(candidatePath)) {
- return candidatePath;
- }
- }
- return null;
- }
- /**
- * Extract import mappings from a file
- */
- export function extractImportMappings(
- _filePath: string,
- content: string,
- language: Language
- ): ImportMapping[] {
- const mappings: ImportMapping[] = [];
- if (language === 'typescript' || language === 'javascript' || language === 'tsx' || language === 'jsx') {
- mappings.push(...extractJSImports(content));
- } else if (language === 'python') {
- mappings.push(...extractPythonImports(content));
- } else if (language === 'go') {
- mappings.push(...extractGoImports(content));
- } else if (language === 'php') {
- mappings.push(...extractPHPImports(content));
- }
- return mappings;
- }
- /**
- * Extract JS/TS import mappings
- */
- function extractJSImports(content: string): ImportMapping[] {
- const mappings: ImportMapping[] = [];
- // ES6 imports
- const importRegex = /import\s+(?:(\w+)\s*,?\s*)?(?:\{([^}]+)\})?\s*(?:(\*)\s+as\s+(\w+))?\s*from\s*['"]([^'"]+)['"]/g;
- let match;
- while ((match = importRegex.exec(content)) !== null) {
- const [, defaultImport, namedImports, star, namespaceAlias, source] = match;
- // Default import
- if (defaultImport) {
- mappings.push({
- localName: defaultImport,
- exportedName: 'default',
- source: source!,
- isDefault: true,
- isNamespace: false,
- });
- }
- // Named imports
- if (namedImports) {
- const names = namedImports.split(',').map((s) => s.trim());
- for (const name of names) {
- const aliasMatch = name.match(/(\w+)\s+as\s+(\w+)/);
- if (aliasMatch) {
- mappings.push({
- localName: aliasMatch[2]!,
- exportedName: aliasMatch[1]!,
- source: source!,
- isDefault: false,
- isNamespace: false,
- });
- } else if (name) {
- mappings.push({
- localName: name,
- exportedName: name,
- source: source!,
- isDefault: false,
- isNamespace: false,
- });
- }
- }
- }
- // Namespace import
- if (star && namespaceAlias) {
- mappings.push({
- localName: namespaceAlias,
- exportedName: '*',
- source: source!,
- isDefault: false,
- isNamespace: true,
- });
- }
- }
- // Require statements
- const requireRegex = /(?:const|let|var)\s+(?:(\w+)|{([^}]+)})\s*=\s*require\(['"]([^'"]+)['"]\)/g;
- while ((match = requireRegex.exec(content)) !== null) {
- const [, defaultName, destructured, source] = match;
- if (defaultName) {
- mappings.push({
- localName: defaultName,
- exportedName: 'default',
- source: source!,
- isDefault: true,
- isNamespace: false,
- });
- }
- if (destructured) {
- const names = destructured.split(',').map((s) => s.trim());
- for (const name of names) {
- const aliasMatch = name.match(/(\w+)\s*:\s*(\w+)/);
- if (aliasMatch) {
- mappings.push({
- localName: aliasMatch[2]!,
- exportedName: aliasMatch[1]!,
- source: source!,
- isDefault: false,
- isNamespace: false,
- });
- } else if (name) {
- mappings.push({
- localName: name,
- exportedName: name,
- source: source!,
- isDefault: false,
- isNamespace: false,
- });
- }
- }
- }
- }
- return mappings;
- }
- /**
- * Extract Python import mappings
- */
- function extractPythonImports(content: string): ImportMapping[] {
- const mappings: ImportMapping[] = [];
- // from X import Y
- const fromImportRegex = /from\s+([\w.]+)\s+import\s+([^#\n]+)/g;
- let match;
- while ((match = fromImportRegex.exec(content)) !== null) {
- const [, source, imports] = match;
- const names = imports!.split(',').map((s) => s.trim());
- for (const name of names) {
- const aliasMatch = name.match(/(\w+)\s+as\s+(\w+)/);
- if (aliasMatch) {
- mappings.push({
- localName: aliasMatch[2]!,
- exportedName: aliasMatch[1]!,
- source: source!,
- isDefault: false,
- isNamespace: false,
- });
- } else if (name && name !== '*') {
- mappings.push({
- localName: name,
- exportedName: name,
- source: source!,
- isDefault: false,
- isNamespace: false,
- });
- }
- }
- }
- // import X
- const importRegex = /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/gm;
- while ((match = importRegex.exec(content)) !== null) {
- const [, source, alias] = match;
- const localName = alias || source!.split('.').pop()!;
- mappings.push({
- localName,
- exportedName: '*',
- source: source!,
- isDefault: false,
- isNamespace: true,
- });
- }
- return mappings;
- }
- /**
- * Extract Go import mappings
- */
- function extractGoImports(content: string): ImportMapping[] {
- const mappings: ImportMapping[] = [];
- // import "path" or import alias "path"
- const singleImportRegex = /import\s+(?:(\w+)\s+)?["']([^"']+)["']/g;
- let match;
- while ((match = singleImportRegex.exec(content)) !== null) {
- const [, alias, source] = match;
- const packageName = source!.split('/').pop()!;
- mappings.push({
- localName: alias || packageName,
- exportedName: '*',
- source: source!,
- isDefault: false,
- isNamespace: true,
- });
- }
- // import ( ... ) block
- const blockImportRegex = /import\s*\(\s*([^)]+)\s*\)/gs;
- while ((match = blockImportRegex.exec(content)) !== null) {
- const block = match[1]!;
- const lineRegex = /(?:(\w+)\s+)?["']([^"']+)["']/g;
- let lineMatch;
- while ((lineMatch = lineRegex.exec(block)) !== null) {
- const [, alias, source] = lineMatch;
- const packageName = source!.split('/').pop()!;
- mappings.push({
- localName: alias || packageName,
- exportedName: '*',
- source: source!,
- isDefault: false,
- isNamespace: true,
- });
- }
- }
- return mappings;
- }
- /**
- * Extract PHP import mappings (use statements)
- */
- function extractPHPImports(content: string): ImportMapping[] {
- const mappings: ImportMapping[] = [];
- // use Namespace\Class; or use Namespace\Class as Alias;
- const useRegex = /use\s+([\w\\]+)(?:\s+as\s+(\w+))?;/g;
- let match;
- while ((match = useRegex.exec(content)) !== null) {
- const [, fullPath, alias] = match;
- const className = fullPath!.split('\\').pop()!;
- mappings.push({
- localName: alias || className,
- exportedName: className,
- source: fullPath!,
- isDefault: false,
- isNamespace: false,
- });
- }
- return mappings;
- }
- // Cache import mappings per file to avoid re-reading and re-parsing
- const importMappingCache = new Map<string, ImportMapping[]>();
- /**
- * Clear the import mapping cache (call between indexing runs)
- */
- export function clearImportMappingCache(): void {
- importMappingCache.clear();
- }
- /**
- * Resolve a reference using import mappings
- */
- export function resolveViaImport(
- ref: UnresolvedRef,
- context: ResolutionContext
- ): ResolvedRef | null {
- // Use cached import mappings or extract and cache them
- let imports = importMappingCache.get(ref.filePath);
- if (!imports) {
- const content = context.readFile(ref.filePath);
- if (!content) {
- return null;
- }
- imports = extractImportMappings(ref.filePath, content, ref.language);
- importMappingCache.set(ref.filePath, imports);
- }
- // Check if the reference name matches any import
- for (const imp of imports) {
- if (imp.localName === ref.referenceName || ref.referenceName.startsWith(imp.localName + '.')) {
- // Resolve the import path
- const resolvedPath = resolveImportPath(
- imp.source,
- ref.filePath,
- ref.language,
- context
- );
- if (resolvedPath) {
- // Find the exported symbol in the resolved file
- const nodesInFile = context.getNodesInFile(resolvedPath);
- const exportedName = imp.isDefault ? 'default' : imp.exportedName;
- // Look for the symbol
- let targetNode: Node | undefined;
- if (imp.isDefault) {
- // Find default export or main class/function
- targetNode = nodesInFile.find(
- (n) => n.isExported && (n.kind === 'function' || n.kind === 'class')
- );
- } else if (imp.isNamespace) {
- // Namespace import - look for the specific member
- const memberName = ref.referenceName.replace(imp.localName + '.', '');
- targetNode = nodesInFile.find(
- (n) => n.name === memberName && n.isExported
- );
- } else {
- // Named import
- targetNode = nodesInFile.find(
- (n) => n.name === exportedName && n.isExported
- );
- }
- if (targetNode) {
- return {
- original: ref,
- targetNodeId: targetNode.id,
- confidence: 0.9,
- resolvedBy: 'import',
- };
- }
- }
- }
- }
- return null;
- }
|