| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813 |
- /**
- * 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, ReExport } from './types';
- import { applyAliases } from './path-aliases';
- /**
- * 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'],
- objc: ['.h', '.m', '.mm'],
- };
- /**
- * 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 — but pass the context so the
- // bare-specifier heuristic can consult the project's tsconfig
- // alias map first (custom prefixes like `@components/*` would
- // otherwise be misclassified as npm).
- if (isExternalImport(importPath, language, context)) {
- 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.)
- *
- * `context` is consulted for project-defined path aliases
- * (tsconfig/jsconfig `paths`). Without that check, custom prefixes
- * like `@components/*` would fail the bare-specifier heuristic and
- * be classified as external before alias resolution can run.
- */
- function isExternalImport(
- importPath: string,
- language: Language,
- context?: ResolutionContext
- ): 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;
- }
- // Project-defined alias prefix? Treat as local.
- const aliases = context?.getProjectAliases?.();
- if (aliases) {
- for (const pat of aliases.patterns) {
- if (importPath.startsWith(pat.prefix)) return false;
- }
- }
- // 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') {
- // Relative imports (rare in idiomatic Go but the grammar allows them).
- if (importPath.startsWith('.')) {
- return false;
- }
- // In-module imports look like `<module-path>/sub/pkg` — local to
- // this project. Without the module-path check we'd flag every
- // cross-package call in a Go monorepo as external (issue #388).
- const mod = context?.getGoModule?.();
- if (mod && (importPath === mod.modulePath || importPath.startsWith(mod.modulePath + '/'))) {
- return false;
- }
- // `internal/` packages stay local even when go.mod is missing —
- // preserves the pre-#388 escape hatch for repos without a parsed module path.
- if (importPath.includes('/internal/')) {
- return false;
- }
- // Anything else is the Go standard library or a third-party module.
- 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).replace(/\\/g, '/');
- // 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.
- *
- * Tries, in order:
- * 1. Project-defined `compilerOptions.paths` (tsconfig/jsconfig).
- * Each pattern can have multiple replacements; tried in tsconfig
- * priority order with extension permutations.
- * 2. The legacy hard-coded fallback list (`@/`, `~/`, `src/`, ...)
- * for projects that have aliases but no tsconfig paths block.
- * 3. Direct path lookup (with extensions).
- */
- function resolveAliasedImport(
- importPath: string,
- projectRoot: string,
- language: Language,
- context: ResolutionContext
- ): string | null {
- const extensions = EXTENSION_RESOLUTION[language] || [];
- const tryWithExt = (basePath: string): string | null => {
- for (const ext of extensions) {
- const candidate = basePath + ext;
- if (context.fileExists(candidate)) return candidate;
- }
- if (context.fileExists(basePath)) return basePath;
- return null;
- };
- // 1. Project tsconfig/jsconfig paths.
- const aliasMap = context.getProjectAliases?.();
- if (aliasMap) {
- const candidates = applyAliases(importPath, aliasMap, projectRoot);
- for (const c of candidates) {
- const hit = tryWithExt(c);
- if (hit) return hit;
- }
- }
- // 2. Hard-coded fallback list. Kept for projects that use these
- // conventional aliases without declaring them in tsconfig.
- const fallbackAliases: Record<string, string> = {
- '@/': 'src/',
- '~/': 'src/',
- '@src/': 'src/',
- 'src/': 'src/',
- '@app/': 'app/',
- 'app/': 'app/',
- };
- for (const [alias, replacement] of Object.entries(fallbackAliases)) {
- if (importPath.startsWith(alias)) {
- const hit = tryWithExt(importPath.replace(alias, replacement));
- if (hit) return hit;
- }
- }
- // 3. Direct path.
- return tryWithExt(importPath);
- }
- /**
- * 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();
- }
- /**
- * Strip JS line + block comments from `content` while preserving
- * string literals (so `"//"` inside a string stays intact). Used by
- * {@link extractReExports} so commented-out export-from statements
- * don't generate phantom re-export edges.
- *
- * Scanner is deliberately small: it only tracks the three contexts
- * relevant for JS/TS — single-quote string, double-quote string, and
- * template literal. Comment recognition is the JS spec subset, no
- * regex-literal awareness (which is fine for our use case: we don't
- * apply this to function bodies, only to top-level files).
- */
- function stripJsComments(content: string): string {
- let out = '';
- let i = 0;
- let str: '"' | "'" | '`' | null = null;
- while (i < content.length) {
- const ch = content[i]!;
- if (str !== null) {
- out += ch;
- if (ch === '\\' && i + 1 < content.length) {
- out += content[i + 1]!;
- i += 2;
- continue;
- }
- if (ch === str) str = null;
- i++;
- continue;
- }
- if (ch === '"' || ch === "'" || ch === '`') {
- str = ch;
- out += ch;
- i++;
- continue;
- }
- if (ch === '/' && content[i + 1] === '/') {
- while (i < content.length && content[i] !== '\n') i++;
- continue;
- }
- if (ch === '/' && content[i + 1] === '*') {
- i += 2;
- while (i < content.length && !(content[i] === '*' && content[i + 1] === '/')) i++;
- i += 2;
- continue;
- }
- out += ch;
- i++;
- }
- return out;
- }
- /**
- * Extract JS/TS re-export declarations from `content`.
- *
- * Recognised forms:
- * export { foo } from './a';
- * export { foo as bar } from './a';
- * export * from './a';
- * export * as ns from './a'; (treated as wildcard for chasing)
- * export { default as Foo } from './a';
- *
- * The walker intentionally stays regex-based — the import-resolver
- * elsewhere in this file already chooses regex over a fresh
- * tree-sitter pass, and this function shares that trade-off. Errors
- * fall through silently; resolution simply skips the broken file.
- */
- export function extractReExports(content: string, language: Language): ReExport[] {
- if (
- language !== 'typescript' &&
- language !== 'javascript' &&
- language !== 'tsx' &&
- language !== 'jsx'
- ) {
- return [];
- }
- const out: ReExport[] = [];
- // Pre-strip block comments + line comments so a commented-out
- // `// export { x } from '...'` doesn't produce a phantom edge.
- // (Template literals are still a possible source of false positives;
- // a project that builds export statements as runtime strings is
- // out of scope.)
- const cleaned = stripJsComments(content);
- // Wildcard: `export * from '...'` or `export * as ns from '...'`
- const wildcardRe = /export\s*\*(?:\s+as\s+\w+)?\s*from\s*['"]([^'"]+)['"]/g;
- let m: RegExpExecArray | null;
- while ((m = wildcardRe.exec(cleaned)) !== null) {
- out.push({ kind: 'wildcard', source: m[1]! });
- }
- // Named: `export { a, b as c } from '...'`
- const namedRe = /export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
- while ((m = namedRe.exec(cleaned)) !== null) {
- const inner = m[1]!;
- const source = m[2]!;
- for (const raw of inner.split(',')) {
- const item = raw.trim();
- if (!item) continue;
- const aliasMatch = item.match(/^(\w+)\s+as\s+(\w+)$/);
- if (aliasMatch) {
- out.push({
- kind: 'named',
- exportedName: aliasMatch[2]!,
- originalName: aliasMatch[1]!,
- source,
- });
- } else if (/^\w+$/.test(item)) {
- out.push({
- kind: 'named',
- exportedName: item,
- originalName: item,
- source,
- });
- }
- }
- }
- return out;
- }
- /**
- * Resolve a reference using import mappings
- */
- export function resolveViaImport(
- ref: UnresolvedRef,
- context: ResolutionContext
- ): ResolvedRef | null {
- // Use cached import mappings (avoids re-reading and re-parsing per ref)
- const imports = context.getImportMappings(ref.filePath, ref.language);
- if (imports.length === 0 && !context.readFile(ref.filePath)) {
- return null;
- }
- // Go cross-package calls: `pkga.FuncX(...)` extracts to referenceName
- // `pkga.FuncX` and the import `github.com/example/myproject/pkga`
- // maps to a *package directory* containing one or more .go files.
- // The generic file-based lookup below can't follow that — issue #388.
- if (ref.language === 'go') {
- const goResult = resolveGoCrossPackageReference(ref, imports, context);
- if (goResult) return goResult;
- }
- // 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) {
- const exportedName = imp.isDefault ? 'default' : imp.exportedName;
- const memberName = imp.isNamespace
- ? ref.referenceName.replace(imp.localName + '.', '')
- : null;
- const targetNode = findExportedSymbol(
- resolvedPath,
- { isDefault: imp.isDefault, isNamespace: imp.isNamespace, exportedName, memberName },
- ref.language,
- context,
- new Set()
- );
- if (targetNode) {
- return {
- original: ref,
- targetNodeId: targetNode.id,
- confidence: 0.9,
- resolvedBy: 'import',
- };
- }
- }
- }
- }
- return null;
- }
- /**
- * Resolve a Go cross-package qualified reference (`pkga.FuncX`) by matching
- * the package alias against an in-module import, stripping the module prefix
- * to a project-relative directory, and locating the exported symbol in any
- * `.go` file under that directory. Returns `null` for stdlib / third-party
- * imports (no `go.mod`-relative match) so the rest of `resolveViaImport`
- * can still try the file-based path.
- */
- function resolveGoCrossPackageReference(
- ref: UnresolvedRef,
- imports: ImportMapping[],
- context: ResolutionContext
- ): ResolvedRef | null {
- const mod = context.getGoModule?.();
- if (!mod) return null;
- // Qualified call: receiver before `.`, member after. A bare reference
- // (no dot) is a same-file/in-package call — handled elsewhere.
- const dotIdx = ref.referenceName.indexOf('.');
- if (dotIdx <= 0) return null;
- const receiver = ref.referenceName.substring(0, dotIdx);
- const memberName = ref.referenceName.substring(dotIdx + 1);
- if (!memberName) return null;
- for (const imp of imports) {
- if (imp.localName !== receiver) continue;
- // Only in-module imports map to a known directory.
- if (imp.source !== mod.modulePath && !imp.source.startsWith(mod.modulePath + '/')) {
- continue;
- }
- const pkgDir = imp.source === mod.modulePath
- ? ''
- : imp.source.substring(mod.modulePath.length + 1);
- // Look up the member by name and pick the candidate whose file lives
- // directly in the package directory. Match the immediate parent dir
- // exactly so a call to `pkga.FuncX` doesn't accidentally land on a
- // `FuncX` declared in `pkga/subpkg/`.
- const candidates = context.getNodesByName(memberName);
- for (const node of candidates) {
- if (node.language !== 'go') continue;
- if (!node.isExported) continue;
- const fp = node.filePath.replace(/\\/g, '/');
- const lastSlash = fp.lastIndexOf('/');
- const fileDir = lastSlash >= 0 ? fp.substring(0, lastSlash) : '';
- if (fileDir === pkgDir) {
- return {
- original: ref,
- targetNodeId: node.id,
- confidence: 0.9,
- resolvedBy: 'import',
- };
- }
- }
- }
- return null;
- }
- /** Recursive depth cap for re-export chain following. Real codebases
- * rarely chain barrels more than 2–3 deep; 8 is a generous safety
- * net that still bounds worst-case work. */
- const REEXPORT_MAX_DEPTH = 8;
- /**
- * Find an exported symbol in `filePath`, following `export { x } from
- * './other'` and `export * from './other'` chains until the original
- * declaration is reached. Cycle-safe via the `visited` set.
- *
- * Without this, every barrel-style import (`import { Foo } from
- * './index'` where `index.ts` only re-exports) used to resolve to
- * nothing — the existing code only looked for declarations IN the
- * resolved file, not declarations the file forwarded.
- */
- function findExportedSymbol(
- filePath: string,
- want: {
- isDefault: boolean;
- isNamespace: boolean;
- exportedName: string;
- memberName: string | null;
- },
- language: Language,
- context: ResolutionContext,
- visited: Set<string>,
- depth = 0
- ): Node | undefined {
- if (depth > REEXPORT_MAX_DEPTH) return undefined;
- if (visited.has(filePath)) return undefined;
- visited.add(filePath);
- const nodesInFile = context.getNodesInFile(filePath);
- // 1. Direct hit: the symbol is declared in this file.
- if (want.isDefault) {
- const direct = nodesInFile.find(
- (n) => n.isExported && (n.kind === 'function' || n.kind === 'class')
- );
- if (direct) return direct;
- } else if (want.isNamespace && want.memberName) {
- const direct = nodesInFile.find(
- (n) => n.name === want.memberName && n.isExported
- );
- if (direct) return direct;
- } else {
- const direct = nodesInFile.find(
- (n) => n.name === want.exportedName && n.isExported
- );
- if (direct) return direct;
- }
- // 2. Re-export hit: the file forwards the symbol to another module.
- const reExports = context.getReExports?.(filePath, language) ?? [];
- if (reExports.length === 0) return undefined;
- // Look for explicit `export { want } from './other'` (with optional rename).
- const targetName = want.isDefault ? 'default' : want.exportedName;
- for (const rex of reExports) {
- if (rex.kind === 'named' && rex.exportedName === targetName) {
- const next = resolveImportPath(rex.source, filePath, language, context);
- if (!next) continue;
- // After rename: `export { foo as bar } from './x'` — to chase
- // `bar`, we look for `foo` in `./x`.
- const chained = findExportedSymbol(
- next,
- {
- isDefault: rex.originalName === 'default',
- isNamespace: false,
- exportedName: rex.originalName,
- memberName: null,
- },
- language,
- context,
- visited,
- depth + 1
- );
- if (chained) return chained;
- }
- }
- // 3. Wildcard re-export: `export * from './other'` — try every
- // forwarding source. This is the barrel-of-barrels case.
- for (const rex of reExports) {
- if (rex.kind === 'wildcard') {
- const next = resolveImportPath(rex.source, filePath, language, context);
- if (!next) continue;
- const chained = findExportedSymbol(next, want, language, context, visited, depth + 1);
- if (chained) return chained;
- }
- }
- return undefined;
- }
|