import-resolver.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. /**
  2. * Import Resolver
  3. *
  4. * Resolves import paths to actual files and symbols.
  5. */
  6. import * as path from 'path';
  7. import { Language, Node } from '../types';
  8. import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping } from './types';
  9. /**
  10. * Extension resolution order by language
  11. */
  12. const EXTENSION_RESOLUTION: Record<string, string[]> = {
  13. typescript: ['.ts', '.tsx', '.d.ts', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'],
  14. javascript: ['.js', '.jsx', '.mjs', '.cjs', '/index.js', '/index.jsx'],
  15. tsx: ['.tsx', '.ts', '.d.ts', '.js', '.jsx', '/index.tsx', '/index.ts', '/index.js'],
  16. jsx: ['.jsx', '.js', '/index.jsx', '/index.js'],
  17. python: ['.py', '/__init__.py'],
  18. go: ['.go'],
  19. rust: ['.rs', '/mod.rs'],
  20. java: ['.java'],
  21. csharp: ['.cs'],
  22. php: ['.php'],
  23. ruby: ['.rb'],
  24. };
  25. /**
  26. * Resolve an import path to an actual file
  27. */
  28. export function resolveImportPath(
  29. importPath: string,
  30. fromFile: string,
  31. language: Language,
  32. context: ResolutionContext
  33. ): string | null {
  34. // Skip external/npm packages
  35. if (isExternalImport(importPath, language)) {
  36. return null;
  37. }
  38. const projectRoot = context.getProjectRoot();
  39. const fromDir = path.dirname(path.join(projectRoot, fromFile));
  40. // Handle relative imports
  41. if (importPath.startsWith('.')) {
  42. return resolveRelativeImport(importPath, fromDir, language, context);
  43. }
  44. // Handle absolute/aliased imports (like @/ or src/)
  45. return resolveAliasedImport(importPath, projectRoot, language, context);
  46. }
  47. /**
  48. * Check if an import is external (npm package, etc.)
  49. */
  50. function isExternalImport(importPath: string, language: Language): boolean {
  51. // Relative imports are not external
  52. if (importPath.startsWith('.')) {
  53. return false;
  54. }
  55. // Common external patterns
  56. if (language === 'typescript' || language === 'javascript' || language === 'tsx' || language === 'jsx') {
  57. // Node built-ins
  58. if (['fs', 'path', 'os', 'crypto', 'http', 'https', 'url', 'util', 'events', 'stream', 'child_process', 'buffer'].includes(importPath)) {
  59. return true;
  60. }
  61. // Scoped packages or bare specifiers that don't start with aliases
  62. if (!importPath.startsWith('@/') && !importPath.startsWith('~/') && !importPath.startsWith('src/')) {
  63. // Likely an npm package
  64. return true;
  65. }
  66. }
  67. if (language === 'python') {
  68. // Standard library modules
  69. const stdLibs = ['os', 'sys', 'json', 're', 'math', 'datetime', 'collections', 'typing', 'pathlib', 'logging'];
  70. if (stdLibs.includes(importPath.split('.')[0]!)) {
  71. return true;
  72. }
  73. }
  74. if (language === 'go') {
  75. // Standard library or external packages
  76. if (!importPath.startsWith('.') && !importPath.includes('/internal/')) {
  77. return true;
  78. }
  79. }
  80. return false;
  81. }
  82. /**
  83. * Resolve a relative import
  84. */
  85. function resolveRelativeImport(
  86. importPath: string,
  87. fromDir: string,
  88. language: Language,
  89. context: ResolutionContext
  90. ): string | null {
  91. const projectRoot = context.getProjectRoot();
  92. const extensions = EXTENSION_RESOLUTION[language] || [];
  93. // Try the path as-is first
  94. const basePath = path.resolve(fromDir, importPath);
  95. const relativePath = path.relative(projectRoot, basePath);
  96. // Try each extension
  97. for (const ext of extensions) {
  98. const candidatePath = relativePath + ext;
  99. if (context.fileExists(candidatePath)) {
  100. return candidatePath;
  101. }
  102. }
  103. // Try without extension (might already have one)
  104. if (context.fileExists(relativePath)) {
  105. return relativePath;
  106. }
  107. return null;
  108. }
  109. /**
  110. * Resolve an aliased/absolute import
  111. */
  112. function resolveAliasedImport(
  113. importPath: string,
  114. _projectRoot: string,
  115. language: Language,
  116. context: ResolutionContext
  117. ): string | null {
  118. const extensions = EXTENSION_RESOLUTION[language] || [];
  119. // Common aliases
  120. const aliases: Record<string, string> = {
  121. '@/': 'src/',
  122. '~/': 'src/',
  123. '@src/': 'src/',
  124. 'src/': 'src/',
  125. '@app/': 'app/',
  126. 'app/': 'app/',
  127. };
  128. // Try each alias
  129. for (const [alias, replacement] of Object.entries(aliases)) {
  130. if (importPath.startsWith(alias)) {
  131. const resolvedPath = importPath.replace(alias, replacement);
  132. // Try with extensions
  133. for (const ext of extensions) {
  134. const candidatePath = resolvedPath + ext;
  135. if (context.fileExists(candidatePath)) {
  136. return candidatePath;
  137. }
  138. }
  139. // Try as-is
  140. if (context.fileExists(resolvedPath)) {
  141. return resolvedPath;
  142. }
  143. }
  144. }
  145. // Try direct path
  146. for (const ext of extensions) {
  147. const candidatePath = importPath + ext;
  148. if (context.fileExists(candidatePath)) {
  149. return candidatePath;
  150. }
  151. }
  152. return null;
  153. }
  154. /**
  155. * Extract import mappings from a file
  156. */
  157. export function extractImportMappings(
  158. _filePath: string,
  159. content: string,
  160. language: Language
  161. ): ImportMapping[] {
  162. const mappings: ImportMapping[] = [];
  163. if (language === 'typescript' || language === 'javascript' || language === 'tsx' || language === 'jsx') {
  164. mappings.push(...extractJSImports(content));
  165. } else if (language === 'python') {
  166. mappings.push(...extractPythonImports(content));
  167. } else if (language === 'go') {
  168. mappings.push(...extractGoImports(content));
  169. } else if (language === 'php') {
  170. mappings.push(...extractPHPImports(content));
  171. }
  172. return mappings;
  173. }
  174. /**
  175. * Extract JS/TS import mappings
  176. */
  177. function extractJSImports(content: string): ImportMapping[] {
  178. const mappings: ImportMapping[] = [];
  179. // ES6 imports
  180. const importRegex = /import\s+(?:(\w+)\s*,?\s*)?(?:\{([^}]+)\})?\s*(?:(\*)\s+as\s+(\w+))?\s*from\s*['"]([^'"]+)['"]/g;
  181. let match;
  182. while ((match = importRegex.exec(content)) !== null) {
  183. const [, defaultImport, namedImports, star, namespaceAlias, source] = match;
  184. // Default import
  185. if (defaultImport) {
  186. mappings.push({
  187. localName: defaultImport,
  188. exportedName: 'default',
  189. source: source!,
  190. isDefault: true,
  191. isNamespace: false,
  192. });
  193. }
  194. // Named imports
  195. if (namedImports) {
  196. const names = namedImports.split(',').map((s) => s.trim());
  197. for (const name of names) {
  198. const aliasMatch = name.match(/(\w+)\s+as\s+(\w+)/);
  199. if (aliasMatch) {
  200. mappings.push({
  201. localName: aliasMatch[2]!,
  202. exportedName: aliasMatch[1]!,
  203. source: source!,
  204. isDefault: false,
  205. isNamespace: false,
  206. });
  207. } else if (name) {
  208. mappings.push({
  209. localName: name,
  210. exportedName: name,
  211. source: source!,
  212. isDefault: false,
  213. isNamespace: false,
  214. });
  215. }
  216. }
  217. }
  218. // Namespace import
  219. if (star && namespaceAlias) {
  220. mappings.push({
  221. localName: namespaceAlias,
  222. exportedName: '*',
  223. source: source!,
  224. isDefault: false,
  225. isNamespace: true,
  226. });
  227. }
  228. }
  229. // Require statements
  230. const requireRegex = /(?:const|let|var)\s+(?:(\w+)|{([^}]+)})\s*=\s*require\(['"]([^'"]+)['"]\)/g;
  231. while ((match = requireRegex.exec(content)) !== null) {
  232. const [, defaultName, destructured, source] = match;
  233. if (defaultName) {
  234. mappings.push({
  235. localName: defaultName,
  236. exportedName: 'default',
  237. source: source!,
  238. isDefault: true,
  239. isNamespace: false,
  240. });
  241. }
  242. if (destructured) {
  243. const names = destructured.split(',').map((s) => s.trim());
  244. for (const name of names) {
  245. const aliasMatch = name.match(/(\w+)\s*:\s*(\w+)/);
  246. if (aliasMatch) {
  247. mappings.push({
  248. localName: aliasMatch[2]!,
  249. exportedName: aliasMatch[1]!,
  250. source: source!,
  251. isDefault: false,
  252. isNamespace: false,
  253. });
  254. } else if (name) {
  255. mappings.push({
  256. localName: name,
  257. exportedName: name,
  258. source: source!,
  259. isDefault: false,
  260. isNamespace: false,
  261. });
  262. }
  263. }
  264. }
  265. }
  266. return mappings;
  267. }
  268. /**
  269. * Extract Python import mappings
  270. */
  271. function extractPythonImports(content: string): ImportMapping[] {
  272. const mappings: ImportMapping[] = [];
  273. // from X import Y
  274. const fromImportRegex = /from\s+([\w.]+)\s+import\s+([^#\n]+)/g;
  275. let match;
  276. while ((match = fromImportRegex.exec(content)) !== null) {
  277. const [, source, imports] = match;
  278. const names = imports!.split(',').map((s) => s.trim());
  279. for (const name of names) {
  280. const aliasMatch = name.match(/(\w+)\s+as\s+(\w+)/);
  281. if (aliasMatch) {
  282. mappings.push({
  283. localName: aliasMatch[2]!,
  284. exportedName: aliasMatch[1]!,
  285. source: source!,
  286. isDefault: false,
  287. isNamespace: false,
  288. });
  289. } else if (name && name !== '*') {
  290. mappings.push({
  291. localName: name,
  292. exportedName: name,
  293. source: source!,
  294. isDefault: false,
  295. isNamespace: false,
  296. });
  297. }
  298. }
  299. }
  300. // import X
  301. const importRegex = /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/gm;
  302. while ((match = importRegex.exec(content)) !== null) {
  303. const [, source, alias] = match;
  304. const localName = alias || source!.split('.').pop()!;
  305. mappings.push({
  306. localName,
  307. exportedName: '*',
  308. source: source!,
  309. isDefault: false,
  310. isNamespace: true,
  311. });
  312. }
  313. return mappings;
  314. }
  315. /**
  316. * Extract Go import mappings
  317. */
  318. function extractGoImports(content: string): ImportMapping[] {
  319. const mappings: ImportMapping[] = [];
  320. // import "path" or import alias "path"
  321. const singleImportRegex = /import\s+(?:(\w+)\s+)?["']([^"']+)["']/g;
  322. let match;
  323. while ((match = singleImportRegex.exec(content)) !== null) {
  324. const [, alias, source] = match;
  325. const packageName = source!.split('/').pop()!;
  326. mappings.push({
  327. localName: alias || packageName,
  328. exportedName: '*',
  329. source: source!,
  330. isDefault: false,
  331. isNamespace: true,
  332. });
  333. }
  334. // import ( ... ) block
  335. const blockImportRegex = /import\s*\(\s*([^)]+)\s*\)/gs;
  336. while ((match = blockImportRegex.exec(content)) !== null) {
  337. const block = match[1]!;
  338. const lineRegex = /(?:(\w+)\s+)?["']([^"']+)["']/g;
  339. let lineMatch;
  340. while ((lineMatch = lineRegex.exec(block)) !== null) {
  341. const [, alias, source] = lineMatch;
  342. const packageName = source!.split('/').pop()!;
  343. mappings.push({
  344. localName: alias || packageName,
  345. exportedName: '*',
  346. source: source!,
  347. isDefault: false,
  348. isNamespace: true,
  349. });
  350. }
  351. }
  352. return mappings;
  353. }
  354. /**
  355. * Extract PHP import mappings (use statements)
  356. */
  357. function extractPHPImports(content: string): ImportMapping[] {
  358. const mappings: ImportMapping[] = [];
  359. // use Namespace\Class; or use Namespace\Class as Alias;
  360. const useRegex = /use\s+([\w\\]+)(?:\s+as\s+(\w+))?;/g;
  361. let match;
  362. while ((match = useRegex.exec(content)) !== null) {
  363. const [, fullPath, alias] = match;
  364. const className = fullPath!.split('\\').pop()!;
  365. mappings.push({
  366. localName: alias || className,
  367. exportedName: className,
  368. source: fullPath!,
  369. isDefault: false,
  370. isNamespace: false,
  371. });
  372. }
  373. return mappings;
  374. }
  375. // Cache import mappings per file to avoid re-reading and re-parsing
  376. const importMappingCache = new Map<string, ImportMapping[]>();
  377. /**
  378. * Clear the import mapping cache (call between indexing runs)
  379. */
  380. export function clearImportMappingCache(): void {
  381. importMappingCache.clear();
  382. }
  383. /**
  384. * Resolve a reference using import mappings
  385. */
  386. export function resolveViaImport(
  387. ref: UnresolvedRef,
  388. context: ResolutionContext
  389. ): ResolvedRef | null {
  390. // Use cached import mappings or extract and cache them
  391. let imports = importMappingCache.get(ref.filePath);
  392. if (!imports) {
  393. const content = context.readFile(ref.filePath);
  394. if (!content) {
  395. return null;
  396. }
  397. imports = extractImportMappings(ref.filePath, content, ref.language);
  398. importMappingCache.set(ref.filePath, imports);
  399. }
  400. // Check if the reference name matches any import
  401. for (const imp of imports) {
  402. if (imp.localName === ref.referenceName || ref.referenceName.startsWith(imp.localName + '.')) {
  403. // Resolve the import path
  404. const resolvedPath = resolveImportPath(
  405. imp.source,
  406. ref.filePath,
  407. ref.language,
  408. context
  409. );
  410. if (resolvedPath) {
  411. // Find the exported symbol in the resolved file
  412. const nodesInFile = context.getNodesInFile(resolvedPath);
  413. const exportedName = imp.isDefault ? 'default' : imp.exportedName;
  414. // Look for the symbol
  415. let targetNode: Node | undefined;
  416. if (imp.isDefault) {
  417. // Find default export or main class/function
  418. targetNode = nodesInFile.find(
  419. (n) => n.isExported && (n.kind === 'function' || n.kind === 'class')
  420. );
  421. } else if (imp.isNamespace) {
  422. // Namespace import - look for the specific member
  423. const memberName = ref.referenceName.replace(imp.localName + '.', '');
  424. targetNode = nodesInFile.find(
  425. (n) => n.name === memberName && n.isExported
  426. );
  427. } else {
  428. // Named import
  429. targetNode = nodesInFile.find(
  430. (n) => n.name === exportedName && n.isExported
  431. );
  432. }
  433. if (targetNode) {
  434. return {
  435. original: ref,
  436. targetNodeId: targetNode.id,
  437. confidence: 0.9,
  438. resolvedBy: 'import',
  439. };
  440. }
  441. }
  442. }
  443. }
  444. return null;
  445. }