Skip to content

fix(ts): follow re-export chains through index.ts barrels - #2900

Open
chidev wants to merge 6 commits into
DeusData:mainfrom
lev-os:fix/ts-reexport-chains-pr
Open

chidev wants to merge 6 commits into
DeusData:mainfrom
lev-os:fix/ts-reexport-chains-pr

Conversation

@chidev

@chidev chidev commented Nov 2, 2026

Copy link
Copy Markdown

Calls through index.ts re-exports now reach the function's defining file. This covers renamed named re-exports (export { a as b } from './m') and export * chains, the cases in #2718 and #1557.

Credit

The first two commits are @antespi's from #2402 (index.ts barrels, dotted file names). I kept his authorship and sign-off unchanged. On October 4 you asked for #2402 to be split into focused PRs. This PR is only the barrel part, plus one commit of mine for renames and export *. @antespi, if you would rather land these yourself, I will close this PR. It only builds on your work.

One claim

This PR changes only TS/JS re-export resolution. It does not include the bare-specifier binding reported in #2869, and it does not change MCP output.

Reproduction

repro.sh (below) writes 6 files: a renamed re-export, an export * chain, and a decoy file with the same names. It uses its own cache and runtime folders.

main (812d075):

a statePath src/other/decoy.ts unique_name "0.38"
b loadThing src/star/thing.impl.ts suffix_match "0.55"

this branch:

a getStatePath src/lib/xdg-paths.ts import_map "0.95"
b loadThing src/star/thing.impl.ts lsp_ts_import "0.95"
repro.sh
#!/usr/bin/env bash
# Calls through index.ts re-exports: a renamed named re-export and an export * chain.
# Usage: bash repro.sh [path/to/codebase-memory-mcp]
# Uses its own cache and runtime folders, so an installed daemon or index is not touched.
set -euo pipefail
BIN="${1:-codebase-memory-mcp}"
W="$(mktemp -d /tmp/cbmreexport.XXXX)"
export CBM_CACHE_DIR="$W/cache" CBM_RUNTIME_DIR="$W/run"
mkdir -p "$CBM_CACHE_DIR" "$CBM_RUNTIME_DIR"; chmod 700 "$CBM_RUNTIME_DIR"
R="$W/repo"; mkdir -p "$R/src/lib" "$R/src/star" "$R/src/other"
echo '{"name":"rel","private":true}' > "$R/package.json"
cat > "$R/src/app.ts" <<'TS'
import { statePath } from './lib';
import { loadThing } from './star';
export function a(): string { return statePath(); }
export function b(): number { return loadThing(); }
TS
echo "export { getStatePath as statePath } from './xdg-paths';" > "$R/src/lib/index.ts"
echo 'export function getStatePath(): string { return "/s"; }' > "$R/src/lib/xdg-paths.ts"
echo "export * from './thing.impl';" > "$R/src/star/index.ts"
echo 'export function loadThing(): number { return 1; }' > "$R/src/star/thing.impl.ts"
# Same names in an unrelated file, so a name guess is visible.
echo 'export function statePath(): string { return "decoy"; } export function loadThing(): number { return 2; }' > "$R/src/other/decoy.ts"
(cd "$R" && git init -q . && git add . && git -c user.email=r@example.invalid -c user.name=repro commit -qm init)
"$BIN" --version
"$BIN" cli --quiet index_repository "{\"repo_path\":\"$R\"}" > "$W/index.json"
P="$(grep -o '"project":"[^"]*"' "$W/index.json" | head -1 | cut -d'"' -f4)"
"$BIN" cli --quiet query_graph "{\"project\":\"$P\",\"query\":\"MATCH (a)-[r:CALLS]->(b) WHERE a.file_path = 'src/app.ts' RETURN a.name, b.name, b.file_path, r.strategy, r.confidence\"}"

Tests

New cases in tests/test_extraction_imports.c and tests/test_lsp_resolution_probe.c. On macOS arm64: scripts/test.sh --suites extraction_imports,edge_imports,path_alias,mcp,lsp_resolution_probe gives 625 passed, 4 skipped. The only failure in a wider run is regex_compile_refuses_overlong_pattern, which fails on main too (#2899).

Every commit is signed off. I have not seen CI on Linux or Windows yet.

antespi and others added 3 commits November 2, 2026 15:36
Signed-off-by: Antonio Espinosa <antespi@gmail.com>
Signed-off-by: Antonio Espinosa <antespi@gmail.com>
Named re-exports (export { a as b } from './m') and star re-exports
(export * from './m') are now recorded as re-export imports instead of
one module-level import. JS and TS call resolution uses the cross-file
import map, so a call through a barrel file reaches the function's
defining file, renames included.

Builds on the index.ts barrel commit from DeusData#2402. Covers the rename and
export * cases in DeusData#2718 and DeusData#1557.

Signed-off-by: chidev <chicagowebdev@gmail.com>
@chidev
chidev requested a review from DeusData as a code owner November 2, 2026 22:40
@chidev chidev mentioned this pull request Nov 2, 2026
3 of 4 tasks
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

The barrel walker kept a visited set of file QNs. A barrel with both
`export * from './m'` and `export { a as b } from './m'` searched m for
`b` through the star edge first, marked m visited, and then skipped m
when the renamed edge asked for `a`. The call to `b` got no edge.

The visited set is now keyed on (file QN, name). Each key is an owned
string stored as its own value and freed with the table.

Tested: new lrp_ts_barrel_star_and_renamed_same_module fails before
(renamed=0) and passes after.

Suites extraction_imports, edge_imports, path_alias, mcp,
lsp_resolution_probe: 626 passed, 4 skipped. The issue repro binds both
calls at 0.95.

Signed-off-by: chidev <chicagowebdev@gmail.com>
Formats the changed lines with clang-format 20.1.8, drops a condition that
is always true (pass_calls.c), and narrows the scope of public_name
(pass_lsp_cross.c), as the lint job reported.

Signed-off-by: chidev <chicagowebdev@gmail.com>
The memory-core lint gate counted new raw allocator sites in the
re-export commits (pass_calls.c 22 -> 28, pass_lsp_cross.c 49 -> 61).

- The barrel walker now returns the graph node it reaches, borrowed for
  the life of the graph buffer, instead of a strdup'd QN. The import map
  stores that node's QN through the existing pxc_import_value_qn copy.
- File and symbol lookups use cbm_pipeline_file_node and a stack-built
  symbol QN (cbm_pipeline_fqn_symbol_prefix_len), so they allocate nothing.
- The visited-set keys are allocated and freed with cbm_alloc/cbm_free
  (CBM_MEM_CLASS_OTHER).
- pass_calls keeps the JS/TS cross-file map as built and frees it with
  cbm_pxc_free_import_map, instead of copying it into a borrowed array.

Both files are back at their baseline counts.

Signed-off-by: chidev <chicagowebdev@gmail.com>
@DeusData

DeusData commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Thank you @chidev, both for this PR and for the way you built it. Carrying @antespi's two commits forward with his authorship and sign-off intact, crediting him in the description, and offering to step back if he wants to land them himself is exactly how we like to see work built on. The repro script with a decoy file makes a name guess visible at a glance. Keying the visited set on (file, name), with its own regression test, and failing closed when two different definitions are reachable are both the right instincts for this code. The dotted-basename fix for alias imports also matches what #1682 already does for relative imports, so that part fits in cleanly.

A few things before this can land:

  1. Walk with the name the import binds, not its local name. The map loop passes the importing file's local_name to pxc_barrel_symbol, which is right only for a plain named import. For import { helper as format } from './utils' the barrel is searched for format: that misses helper, and if another module behind the same export * happens to define format, the call binds to the wrong function. A default import (import X from './lib') and a namespace import (import * as ns from './lib') are searched by their local names in the same way, although export * never re-exports default. Could the walk look up the importing file's CBMImport for that local and use its member for named imports, and skip default and namespace imports? A negative test would pin it: an aliased import whose alias matches a name that another star-exported module defines must still bind to the member.

  2. Please drop the depth cap. PXC_BARREL_MAX_DEPTH decides what ends up in the graph (a chain of nine re-export hops gets no edge, whatever the code says), and the project's rule is that no work cap decides graph content. Your (file, name) visited set already makes the walk finite as a property of the input, so the cap is not needed for termination. Removing it would leave the recursion depth up to the input, so an explicit worklist bounded by the visited set would be the safest shape. The Python re-export following in cbm_pxc_filter_defs_for_file (commit 0ae936f) uses that pattern.

  3. Cost across files. The walk is repeated for every file that imports through a barrel, on every import-map build (the CALLS map, and again for files with an import type), so a shared index.ts with many export * lines is re-walked by each importer. That is files times barrel size, the kind of coupling our complexity guard exists to catch, and the guard has no TypeScript fixture yet. Could the (barrel, name) -> definition result be computed once per pass, after the IMPORTS edges exist, and then only read by the resolve workers? That is the same shape the shared cross-file registries use.

  4. Incremental parity. An importer's CALLS now depend on the barrel's re-export lines, not only on the defining file. Could you add an incremental test that changes a barrel's export * from to point at a different module, with no definitions changed, and asserts that the importer's CALLS match a clean rebuild? If that fails, the barrel's re-exports probably need to count as a change for its importers, the way a changed Python namespace declines the incremental closure today.

  5. Smaller items:

    • A re-export does not bind its name inside the barrel (export { x } from './m' creates no local x), but the barrel's own import map now binds it. Could re-export records be left out of local binding?
    • An alias import that spells out /index (for example @app/lib/index) now goes through cbm_pipeline_fqn_folder, which keeps the index segment that cbm_pipeline_fqn_module used to drop. A quick test that such an import still reaches lib/index.ts would settle it.
    • The description says MCP output does not change. The IMPORTS edges for re-export statements do change: one edge per specifier, local_name is the exported name (or *), and there are new reexport and imported properties. That is fine, but please mention it in the description.

Fine to leave for a follow-up: export { default as Button } from './Button' (very common in React barrels) and a local export { x } without from.

Once these are in, we will run a before/after census of CALLS and IMPORTS on a real barrel-heavy TypeScript repository, through both the sequential and the parallel resolver. Thanks again, this closes a long-standing gap for NestJS and monorepo users.

This branch has not been deployed

No deployments
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.

3 participants