Skip to content

fix(ts): a re-export imports its module, and sync follows barrels of barrels - #2526

Merged
colbymchenry merged 1 commit into
mainfrom
claude/sync-nested-esm-barrels
Oct 12, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/sync-nested-esm-barrels

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

An export … from './x' statement left almost nothing in the graph. Both extractors emitted one imports reference per re-exported name (export { A } from './x' → A). They emitted nothing for the module itself, and nothing at all for export * from './x'. Two things followed:

Fix

  • A re-export imports its module, as import … from does, in both extractors:
    • src/extraction/tree-sitter.ts calls extractImport for an export_statement with a source.
    • codegraph-kernel/src/tsjs/mod.rs calls extract_import for one.
    • Each creates an import node named by the specifier and a module reference, which links the barrel to the file it names, as an import's module reference does. The per-name references stay as they were.
    • The import node also lets a bare or scoped specifier ('reka-ui', '@scope/pkg') pass the resolver's name pre-filter, as an import statement's does.
  • Sync follows modules that re-export a changed one (src/resolution/module-importers.ts):
    • Through the new edges, level by level, carrying the names each passes on: export * passes everything, a named or renamed re-export its exported name, export * as ns the namespace.
    • Up to REEXPORT_MAX_DEPTH, as far as the resolver's findExportedSymbol chases a re-export.
    • An ES namespace import (import * as z from './index') reads whatever its module passes on. Python keeps fix(sync): imports through a re-exporting module follow it when it is deleted or added #2512's rules.
  • The "parse produced no symbols" warning doesn't count a re-export's import node. A barrel with an unparseable line (export type * from, TypeScript 'export type * from' makes the whole file parse to no symbols #2474) still reports it, as status-index-health.test.ts pins.

Fresh indexes change for JavaScript and TypeScript files with export … from statements: one import node, its contains edge, and a module edge (or a failed reference) per statement. The CHANGELOG asks users to re-index.

Validation

Tests

  • __tests__/extraction.test.ts:
    • a re-export creates the import node and module reference, for export *, a named re-export and export * as;
    • an export *-only barrel is a dependent of its module.
  • __tests__/sync-module-importers.test.ts, ES-module fixture: an outer src/ui/index.ts (export * plus a renamed re-export), and kit.ts passing one name on, read through a namespace import. Deleted, restored and added late, each compared with a fresh index of the final tree in a second folder.
  • On main, 4 of these fail.
  • Each part has teeth, checked by mutation:
    • without the TS extractor's import: 4 tests fail (kernel off);
    • without the kernel's: 4 kernel parity tests and the 2 nested sync tests fail (kernel on);
    • without following ES re-exports: 2 fail;
    • without the namespace rule: 2 fail.
  • They pass with the kernel on and off.
  • Kernel parity (kernel-tsjs-parity, kernel-node-id-parity, kernel-grammar-parity) passes, including real-source parity on src/mcp/tools.ts.
  • Full suite on Windows: 6,939 passed, 82 skipped. The one failure, git-index-currency (status.pendingChanges reports zero for a committed-but-unindexed file — the git fast path never hash-checks what git omits #1829, timing-based, run while the A/B loaded the machine), passed 13/13 twice in isolation.

Fresh-index A/B (main and this PR, kernel on). The only differences are the expected ones:

repo import nodes contains edges module edges failed module refs
vben +679 +679 +625 +54
excalidraw +171 +171 +155 +16
zod +138 +138 +137 +1
vue3-element-admin +38 +38 +37 +1
  • Plus each file row's node count.
  • No other node, edge or reference moved.
  • takenote, requests, gin and serilog are byte-identical.
  • The failed module refs are external packages (reka-ui, vue-i18n, zod) or workspace packages whose import statements fail the same way on main (@excalidraw/element: 414 failed import refs, none resolved).

Sync A/B. Each arm's synced graph is compared with its own fresh index of the same tree. Cells are differing graph lines, main → this PR. Barrels are every 2nd index.{ts,tsx,js,jsx}; the rest are every 7th source file.

repo barrels deleted every 7th deleted barrels restored every 7th restored barrels added late every 7th added late
zod 324 → 0 86 → 30 0 → 0 10 → 0 324 → 0 66 → 0
vben 560 → 560 279 → 273 0 → 0 100 → 96 478 → 478 220 → 210
excalidraw 5,399 → 5,399 3,693 → 3,693 44 → 44 24 → 24 5,438 → 5,438 3,502 → 3,502
vue3-element-admin 0 → 0 8 → 8 0 → 0 0 → 0 0 → 0 0 → 0
takenote 16 → 16 12 → 12 0 → 0 0 → 0 0 → 0 0 → 0
  • In every scenario this PR adds no residue line that main doesn't have; it only removes them.
  • Controls (requests in Python, gin in Go, serilog in C#; every 7th deleted, restored and added late): synced graphs byte-identical to main's in all 9 runs.

Cost. Sync wall time, main and this PR, two interleaved rounds:

scenario main this PR
zod, 9 barrels deleted 1.49 / 1.48 s 1.58 / 1.60 s
zod, 9 barrels added late 1.99 / 1.97 s 2.12 / 2.12 s
vben, 114 barrels deleted 3.04 / 3.06 s 3.17 / 3.13 s
excalidraw, 17 barrels deleted 2.36 / 2.36 s 2.42 / 2.40 s

Fresh-index time is unchanged within noise (vben 11.6 → 11.1 s, excalidraw 11.5 → 11.4 s, zod 8.3 → 8.2 s).

Residues

Overlap

🤖 Generated with Claude Code

…barrels

An `export … from './x'` statement emitted one `imports` reference per
re-exported name and nothing for the module itself; `export * from` emitted
nothing at all. So a barrel made only of `export *` lines had no dependency
on what it re-exports (no dependent for `affected`), and sync could not find
an `index.ts` that re-exports another barrel: when the inner one was deleted
or added, the outer one's importers kept their stale or name-matched edges.

A re-export now imports its module the way `import … from` does, in both
extractors (TS and the Rust kernel): an import node named by the specifier
and a module reference that links the barrel to the file it names. Sync's
importer discovery follows such modules to their own importers, carrying
the names each passes on (`export *`, a named or renamed re-export, an
`export * as ns`), as deep as the resolver chases a re-export; an ES
namespace import reads whatever its module passes on. A re-export's import
node does not count as a symbol for the "parse produced no symbols" warning,
so a barrel with an unparseable line still reports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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