* 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`
(
filePath: string,
want: {
isDefault: boolean;
isNamespace: boolean;
exportedName: string;
memberName: string | null;
},
language: Language,
context: ResolutionContext,
visited: Set<string>,
depth = 0
)
| 1829 | * resolved file, not declarations the file forwarded. |
| 1830 | */ |
| 1831 | function findExportedSymbol( |
| 1832 | filePath: string, |
| 1833 | want: { |
| 1834 | isDefault: boolean; |
| 1835 | isNamespace: boolean; |
| 1836 | exportedName: string; |
| 1837 | memberName: string | null; |
| 1838 | }, |
| 1839 | language: Language, |
| 1840 | context: ResolutionContext, |
| 1841 | visited: Set<string>, |
| 1842 | depth = 0 |
| 1843 | ): Node | undefined { |
| 1844 | if (depth > REEXPORT_MAX_DEPTH) return undefined; |
| 1845 | if (visited.has(filePath)) return undefined; |
| 1846 | visited.add(filePath); |
| 1847 | |
| 1848 | const nodesInFile = context.getNodesInFile(filePath); |
| 1849 | |
| 1850 | // 1. Direct hit: the symbol is declared in this file. |
| 1851 | if (want.isDefault) { |
| 1852 | // Svelte/Vue single-file components ARE the module's default export, |
| 1853 | // but are extracted as kind 'component' (not function/class). Prefer |
| 1854 | // the component node; fall back to an exported function/class for the |
| 1855 | // `.ts`/`.tsx` `export default fn`/`class` case. Without the component |
| 1856 | // branch, an `export { default as X } from './X.svelte'` barrel never |
| 1857 | // resolves and the component shows a false 0 callers (#629). |
| 1858 | const direct = |
| 1859 | nodesInFile.find((n) => n.isExported && n.kind === 'component') ?? |
| 1860 | nodesInFile.find( |
| 1861 | (n) => n.isExported && (n.kind === 'function' || n.kind === 'class') |
| 1862 | ); |
| 1863 | if (direct) return direct; |
| 1864 | } else if (want.isNamespace && want.memberName) { |
| 1865 | const direct = nodesInFile.find( |
| 1866 | (n) => n.name === want.memberName && n.isExported |
| 1867 | ); |
| 1868 | if (direct) return direct; |
| 1869 | } else { |
| 1870 | const direct = nodesInFile.find( |
| 1871 | (n) => n.name === want.exportedName && n.isExported |
| 1872 | ); |
| 1873 | if (direct) return direct; |
| 1874 | } |
| 1875 | |
| 1876 | // 2. Re-export hit: the file forwards the symbol to another module. |
| 1877 | const reExports = context.getReExports?.(filePath, language) ?? []; |
| 1878 | if (reExports.length === 0) return undefined; |
| 1879 | |
| 1880 | // Look for explicit `export { want } from './other'` (with optional rename). |
| 1881 | const targetName = want.isDefault ? 'default' : want.exportedName; |
| 1882 | for (const rex of reExports) { |
| 1883 | if (rex.kind === 'named' && rex.exportedName === targetName) { |
| 1884 | const next = resolveImportPath(rex.source, filePath, language, context); |
| 1885 | if (!next) continue; |
| 1886 | // After rename: `export { foo as bar } from './x'` — to chase |
| 1887 | // `bar`, we look for `foo` in `./x`. |
| 1888 | const chained = findExportedSymbol( |
no test coverage detected