Skip to content

fix(resolution): a barrel's export { A } from './x' links what ./x exports, not a namesake - #2685

Merged
colbymchenry merged 1 commit into
mainfrom
claude/named-reexport-module
Oct 19, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/named-reexport-module

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

A barrel's own export { A, B as C } from './x' emits one imports reference per name it passes on (A, B: the names as ./x calls them). #2526 added the statement's import node and module edge, and left those per-name references as they were.

No import statement binds a re-exported name, so resolveViaImport never matched one, and each was looked up by its name alone. That picked whichever same-named symbol the name matcher liked:

Fix

  • resolveViaImport (src/resolution/import-resolver.ts): a JS/TS imports reference that no import binds, and that the file re-exports by that name from exactly one module, resolves through that module. That's resolveImportPath plus findExportedSymbol, which follows barrels of barrels and renames (astro's render is renderTemplate as render one barrel further). A name two statements re-export from different modules keeps the old lookup.
  • isOutOfRepoBinding (src/resolution/name-matcher.ts): a name re-exported from an outside package counts as bound to that package, as an imported one already does. So it never name-matches a project namesake.
  • The shared helper reExportSource reads the re-exports the resolver already parses (getReExports, the same table findExportedSymbol chases for importers). It lives in name-matcher, which import-resolver already imports from.
  • Sync re-opens those names when their module changes (src/resolution/module-importers.ts, bindingsOf).

Extraction doesn't change, so neither does the kernel. An existing index keeps its old links until the barrel changes or the project is re-indexed, so the CHANGELOG says to re-index.

Verification

A/B on real repos

Arms: origin/main 16d1c95, and this branch on it. They differ only in import-resolver.js and name-matcher.js, and both carry the same kernel. Each repo was indexed fresh with codegraph init -y and dumped with scripts/dump-graph.mjs. Edges are paired by kind, source, line, column and name.

16 byte-identical: ghostfolio, RuoYi-Vue3, mantis, matx, takenote, react-redux-realworld, proshop-v2, supertokens/dashboard, Reactime, mihailgaberov/chat, ziya, kkFileView, Games-and-Go, Mimi.jl, pypa/build (Python) and phpip (PHP).

14 changed. Every change is a barrel's own imports edge; no other edge, node or file moved.

Repo Same target, now through the module Moved to the module's export Now resolved (failed before) Package re-export no longer on a namesake
luci-go milo/ui 107 4 1
excalidraw 163 3 2
vben 26
zod 130 12
Folo 135 3 2
astro (packages/astro) 221 3
nitro 32 1 1
vitepress 8
SvelteKit (packages/kit) 35 29 1
docusaurus 10
inventree frontend 54 1
bulletproof-react 9
create-t3-turbo 2
vue-element-admin 2

Same target (934): these edges kept their target and now resolve through the module (resolvedBy: import, 0.9) instead of by name (exact-match 0.5–0.9, or the React framework rule's 0.8).

Moved (55): I checked about 25 against the source and found no wrong move:

  • milo: the commit table's two providers now reach src/gitiles/.../context/context.tsx. Also InvocationProvider reaches ./provider (not common/store/invocation_state), and Component reaches health_page.tsx (not a Lit interface).
  • zod (12): v4/mini/parse.ts re-exports parse, encode, decode and the rest from ../core/index.js, and now reaches core/parse.ts (not classic/parse.ts).
  • SvelteKit (29): the navigation functions now reach runtime/client/client.js.
  • excalidraw:
    • Sidebar reaches components/Sidebar/Sidebar.tsx, where main had an example app's sidebar;
    • CommandPalette reaches components/CommandPalette/CommandPalette.tsx, where main had a menu item;
    • ChatMessage reaches the component, where main had a type alias.
  • Folo:
    • RootPortal reaches packages/internal/components/src/ui/portal, where main had the landing app's;
    • EmptyState reaches ChatSessionComponents.tsx;
    • ShortcutModalContent reaches its own folder's file.
  • astro: defineAction and getActionContext reach actions/runtime/server.ts, where main had the client entrypoint. render reaches renderTemplate.
  • nitro: runTask reaches src/task.ts.

Now resolved (5): previously failed refs such as milo's search_input/index.ts SearchInput.

No longer linked (3): all re-exports from packages: Folo's electron-log (2) and nitro's hookable.

Sync A/B. In #2526's two scenarios, every 2nd barrel index.{ts,tsx,js,jsx} with an export … from is either deleted after indexing, or added back after an index built without it. Each arm's synced graph is compared with its own fresh index of the final tree.

Repo (barrels) deleted: main → this PR added late: main → this PR
zod (5) 0 → 0 0 → 0
excalidraw (6) 0 → 0 0 → 0
luci-go milo/ui (152) 57 → 57 6 → 6

milo's drift lines are byte-identical across arms. That drift exists on main and isn't from re-exports.

Cost: none measurable. Each re-exported name does one table lookup plus the module resolution an import already does.

Overlap

Open #2483 makes type-only re-exports (export type { X } from) visible to extractReExports. If it lands, those names resolve through their module by this rule too, with no change here.

🤖 Generated with Claude Code

…ports, not a namesake

A barrel's own export { A, B as C } from './x' emits one imports
reference per name it passes on, named as ./x calls it. No import binds
a re-exported name, so the resolver looked each one up by its name alone
and could reach a namesake elsewhere in the project, or a project
function named like a package's export.

Each such name now resolves through the module the statement names, as
an import does, following barrels of barrels and renames. A name passed
on from an outside package counts as bound to that package, so it never
name-matches a project symbol. A name two statements re-export from
different modules keeps the old lookup.

Sync re-opens a barrel's re-exported names when the module they go
through is deleted or added, as it already did for the names an importer
binds, so a synced graph keeps matching a fresh index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 951d575 into main Oct 19, 2026
@colbymchenry
colbymchenry deleted the claude/named-reexport-module branch October 19, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant