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>
@colbymchenry
colbymchenry merged commit 041db10 into main Oct 12, 2026
@colbymchenry
colbymchenry deleted the claude/sync-nested-esm-barrels branch October 12, 2026 16:56
colbymchenry added a commit that referenced this pull request Oct 23, 2026
…parses (#2837)

TypeScript 5.0's type-only re-export of a whole module, `export type * from
'./x'` and `export type * as ns from './x'`, has no rule in the vendored
tree-sitter-typescript grammar (v0.23.2, the revision the kernel's crate
compiles) or in upstream master: `type` parses as an ERROR inside the export
statement. The graph was already right, since the import resolver reads the
statement from the file (PRs #1955 and #2526), but the error tree meant a
barrel of re-exports got the "parse produced no symbols" warning and a
parse_error row, `codegraph status` listed it among the files it could not
parse, and the native kernel deferred every file with such a line to wasm.

The TypeScript extractor's preParse (.ts and .tsx) now blanks that `type` to
four spaces where a statement starts (a line start, or after `;`, `{`, `}` or
a block comment) when `*` is followed by `from` or `as`, so the line reads as
the `export *` it is to the graph. Offsets are kept. The kernel route applies
the preParse before calling the kernel, so both engines parse the same bytes
and no Rust change is needed. A grammar patch would mean vendoring and
regenerating the grammar for both dialects, two wasm rebuilds and a kernel
off the crates.io crate, for a statement `export *` already models. A
literal pre-check skips other files: 16 ms over strapi's 4,659 TS files.

The only node change is an import node's stored statement text, which now
reads `export      * from './x'`. Import nodes are filtered from search,
explore and the context builder, and nothing in resolution reads it.

status-index-health.test.ts used `export type * from` as its unparseable
file (issue #2336); it now uses a file with merge-conflict markers.

Validation:
- New ts-export-type-star.test.ts, 15 tests: 13 fail with main's
  typescript.ts, 11 with the blank unwired, all pass with the fix (kernel
  and wasm, .ts and .tsx, LF and CRLF).
- A/B, kernel engine, main vs fix, dumps by natural keys: masto.js,
  kysely, TanStack/query, graffle and strapi keep every edge and ref
  (162,421 and 192,248 on strapi). The only changes are 883 import
  signatures, one per `export type *` statement, and 76 cleared warnings
  (masto.js 11, kysely 1, query 4, graffle 28, strapi 32). The Go and
  Python controls gin and requests are byte-identical.
- Kernel parity, typescript and tsx: 0 diffs on every sweep. Deferrals
  drop masto.js 11 to 0, graffle 203 to 13, strapi 105 to 69, query 32
  to 27, kysely 14 to 12; the rest are other grammar gaps.
- Index time, median of 3 interleaved: graffle 12.7 s vs 13.1 s, query
  11.2 s vs 11.4 s, within noise.

Fixes #2474

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