Files
codegraph/src/resolution/import-resolver.ts
T
Olaf MonienandClaude Opus 4.6 ab9d7188c1 perf: Cache import mappings and index fuzzy matches for resolution
Two major optimizations for the ref resolution phase:

1. Cache extractImportMappings() results per file path — previously
   re-read and re-parsed the source file for every single ref from
   that file (e.g. 100 refs from one file = 100 identical file reads)

2. Replace linear scan in matchFuzzy() with a lazily-built
   case-insensitive Map index — O(1) lookup instead of iterating
   all function/method/class nodes for every unresolved ref.
   Also drop low-value prefix matching (confidence 0.3).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:29:18 +01:00

507 lines
13 KiB
TypeScript

/**
* 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;
}