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

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>
This commit is contained in:
Colby Mchenry
2026-10-12 16:56:50 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 7ba8eee4a7
commit 041db10146
7 changed files with 108 additions and 39 deletions
+18 -8
View File
@@ -564,6 +564,8 @@ export class TreeSitterExtractor {
private fileScopeValueCounts = new Map<string, number>(); // file-scope nodes per name (conditional-def detection)
private valueRefScopes: Array<{ id: string; node: SyntaxNode; name: string }> = [];
private errors: ExtractionError[] = [];
// Import nodes made for `export … from` statements: no symbols of the file's own.
private reExportImports = 0;
private extractor: LanguageExtractor | null = null;
private nodeStack: string[] = []; // Stack of parent node IDs
// C/C++ enclosing `namespace ns { … }` names, prepended to every contained
@@ -733,7 +735,9 @@ export class TreeSitterExtractor {
this.unresolvedReferences.push(...commonJsRequireRefs(this.filePath, this.source, this.language));
// hasError is routine for several grammars; warn only when no symbols survived.
const symbolCount = this.nodes.filter((n) => n.kind !== 'file').length;
// (A re-export's import of its module is none: a barrel whose other lines
// failed to parse has lost what it passes on.)
const symbolCount = this.nodes.filter((n) => n.kind !== 'file').length - this.reExportImports;
if (this.tree?.rootNode.hasError && symbolCount === 0) {
this.errors.push({
message:
@@ -1577,13 +1581,16 @@ export class TreeSitterExtractor {
else if (this.extractor.importTypes.includes(nodeType)) {
this.extractImport(node);
}
// Re-export from another module — `export { X } from './y'` (TS/JS). A
// re-export is a dependency on the source module just like an import, but
// the export_statement is otherwise only descended into (no declaration to
// extract), so a barrel that ONLY re-exports produced zero edges and showed
// 0 dependents. Link each re-exported name to its definition. Children are
// still visited (a non-re-export `export const X = …` has no `source` and
// falls through to its normal declaration extraction).
// Re-export from another module — `export { X } from './y'`, `export * from
// './y'` (TS/JS). A re-export is a dependency on the source module just like
// an import, but the export_statement is otherwise only descended into (no
// declaration to extract), so a barrel that ONLY re-exports produced zero
// edges and showed 0 dependents. It imports the module as `import … from`
// does — an import node and a reference to the module, which links the
// barrel to the file it names (so a barrel of barrels can be followed) —
// and links each re-exported name to its definition. Children are still
// visited (a non-re-export `export const X = …` has no `source` and falls
// through to its normal declaration extraction).
else if (
nodeType === 'export_statement' &&
(this.language === 'typescript' || this.language === 'tsx' ||
@@ -1591,6 +1598,9 @@ export class TreeSitterExtractor {
this.language === 'arkts') &&
getChildByField(node, 'source')
) {
const before = this.nodes.length;
this.extractImport(node);
this.reExportImports += this.nodes.length - before;
const parentId = this.nodeStack[this.nodeStack.length - 1];
if (parentId) this.emitReExportRefs(node, parentId);
}