Skip to content

TypeScript: an import resolves to a same-named method of an exported object literal instead of the exported function #2945

Closed

Description

@inth3shadows

import { f } from './m' resolves to a method named f inside an exported object literal in m.ts, instead of m.ts's exported function f, when the object comes first in the file.

// nav.ts
export interface Driver { symbolHref(id: string): string; }
export const hashDriver: Driver = {
  symbolHref(id) { return '#' + id; },          // line 3
};
let driver: Driver = hashDriver;
export function symbolHref(id: string): string { return driver.symbolHref(id); }   // line 6

// walk.ts
import { symbolHref } from './nav';
export function walkTo(id: string) { return symbolHref(id); }
expected actual
walkTo calls symbolHref at nav.ts:6 symbolHref at nav.ts:3 (hashDriver::symbolHref, resolvedBy: import)

Reproduced on main at 31ec908 with CODEGRAPH_KERNEL=0, and on 69af19a with the native kernel too (Linux/WSL2, Node 22.23.2). The same thing happens in this repo: ui/src/lib/walk.ts calls symbolHref/navigate, and __tests__/ui-package.test.ts calls symbolHref/fileHref/mapHref/flowHref. All of them land on hashNavigation's methods in ui/src/lib/navigation.ts (lines 123–187) rather than the exported functions at lines 240–272.

Cause. The object's method is indexed as function symbolHref, isExported = 1, qualifiedName hashDriver::symbolHref (presumably through the object-literal extraction for an exported const). getFileExportIndex keeps the first exported node per name (import-resolver.ts#L279), so the method shadows the real export.

Possible fix. When two exported nodes share a name, let a top-level declaration replace a member:

const prev = idx.byName.get(n.name);
if (!prev || (prev.qualifiedName !== prev.name && n.qualifiedName === n.name)) idx.byName.set(n.name, n);

With that change walkTo links to nav.ts:6, and the 62 __tests__ files matching import/export/reexport/barrel/store/commonjs/require/default pass (490 tests, CODEGRAPH_KERNEL=0). I haven't run the full suite, and a stricter version might keep members out of byName entirely.

Not fixed by #2754 or #2800: I ran this fixture on both heads and the edge still lands on line 3. I also searched open issues and PRs for object-literal methods and import namesakes, and found nothing else that covers it.

Activity

  1. added a commit that references this issue on Oct 30, 2026
    59eb0d2
  2. colbymchenry commented on Oct 30, 2026

    @colbymchenry
    Owner

    Thanks for the report and the precise repro! An import now links to what the module itself exports under that name (a function, an export { … } clause, export default, module.exports or a re-export) rather than to a same-named method of an exported object. The fix is #3076; it's on main and ships in the next release. Re-index JavaScript and TypeScript projects after upgrading.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions