mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 06:46:49 +08:00
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:
co-authored by
Claude Opus 5.5
parent
7ba8eee4a7
commit
041db10146
@@ -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);
|
||||
}
|
||||
|
||||
@@ -2732,7 +2732,7 @@ function findGoPackageMember(
|
||||
/** Recursive depth cap for re-export chain following. Real codebases
|
||||
* rarely chain barrels more than 2–3 deep; 8 is a generous safety
|
||||
* net that still bounds worst-case work. */
|
||||
const REEXPORT_MAX_DEPTH = 8;
|
||||
export const REEXPORT_MAX_DEPTH = 8;
|
||||
|
||||
/**
|
||||
* Find an exported symbol in `filePath`, following `export { x } from
|
||||
|
||||
@@ -11,20 +11,18 @@
|
||||
* binds them through the module when it is there, and another way (by name)
|
||||
* or not at all when it is not.
|
||||
*
|
||||
* A Python module that re-exports another one (`from .shop import *`, or a
|
||||
* package's `__init__.py` importing names from it) is the way in for its own
|
||||
* importers, so theirs are followed too, as deep as the resolver chases a
|
||||
* name through such modules. An ES module's `export … from` leaves no import
|
||||
* edge or node to find it by, so an `index.ts` re-exporting another is not
|
||||
* followed.
|
||||
* A module that re-exports another one (`export * from './components'` in an
|
||||
* `index.ts`, `from .shop import *` or a package's `__init__.py` importing
|
||||
* names from it) is the way in for its own importers, so theirs are followed
|
||||
* too, as deep as the resolver chases a name through such modules.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { QueryBuilder } from '../db/queries';
|
||||
import type { Language, UnresolvedReference } from '../types';
|
||||
import type { ImportMapping } from './types';
|
||||
import { extractImportMappings, pythonModulePath } from './import-resolver';
|
||||
import type { ImportMapping, ReExport } from './types';
|
||||
import { extractImportMappings, extractReExports, pythonModulePath, REEXPORT_MAX_DEPTH } from './import-resolver';
|
||||
import { resurrectRefFromDroppedEdge } from './resurrect-ref';
|
||||
|
||||
/**
|
||||
@@ -37,11 +35,12 @@ const BINDING_LANGUAGES = new Set<Language>([
|
||||
]);
|
||||
|
||||
/**
|
||||
* How many re-exporting Python modules deep the importers of a file are
|
||||
* followed: as deep as the resolver chases a name through star imports and
|
||||
* re-exports (`pythonModuleSymbol`).
|
||||
* How many re-exporting modules deep the importers of a file are followed: as
|
||||
* deep as the resolver chases a name through them, three for Python's star
|
||||
* imports and re-exports (`pythonModuleSymbol`), `REEXPORT_MAX_DEPTH` for
|
||||
* ES-module `export … from`.
|
||||
*/
|
||||
const PYTHON_REEXPORT_DEPTH = 3;
|
||||
const reExportDepth = (language: Language): number => (language === 'python' ? 3 : REEXPORT_MAX_DEPTH);
|
||||
|
||||
/** A Python star import, read the way the resolver reads one. */
|
||||
const PYTHON_STAR_IMPORT = /^\s*from\s+([\w.]+)\s+import\s+\*/gm;
|
||||
@@ -72,12 +71,14 @@ type AddImporter = (importer: string, language: Language, source: string, module
|
||||
export interface ImportReader {
|
||||
/** Its import mappings. */
|
||||
mappings(file: string, language: Language): ImportMapping[];
|
||||
/** Its ES-module `export … from` statements, which have no import mapping. */
|
||||
reExports(file: string, language: Language): ReExport[];
|
||||
/** The modules a Python file star-imports, which have no import mapping. */
|
||||
starImports(file: string, language: Language): string[];
|
||||
}
|
||||
|
||||
export function createImportReader(rootDir: string): ImportReader {
|
||||
const memo = new Map<string, { mappings: ImportMapping[]; starImports: string[] }>();
|
||||
const memo = new Map<string, { mappings: ImportMapping[]; reExports: ReExport[]; starImports: string[] }>();
|
||||
const read = (file: string, language: Language) => {
|
||||
let entry = memo.get(file);
|
||||
if (!entry) {
|
||||
@@ -89,6 +90,7 @@ export function createImportReader(rootDir: string): ImportReader {
|
||||
}
|
||||
entry = {
|
||||
mappings: content ? extractImportMappings(file, content, language) : [],
|
||||
reExports: content ? extractReExports(content, language) : [],
|
||||
starImports: language === 'python' ? [...content.matchAll(PYTHON_STAR_IMPORT)].map((m) => m[1]!) : [],
|
||||
};
|
||||
memo.set(file, entry);
|
||||
@@ -97,6 +99,7 @@ export function createImportReader(rootDir: string): ImportReader {
|
||||
};
|
||||
return {
|
||||
mappings: (file, language) => read(file, language).mappings,
|
||||
reExports: (file, language) => read(file, language).reExports,
|
||||
starImports: (file, language) => read(file, language).starImports,
|
||||
};
|
||||
}
|
||||
@@ -177,12 +180,11 @@ function importersOf(
|
||||
}
|
||||
const python = level.filter((module) => module.language === 'python');
|
||||
if (python.length > 0) addPythonImporters(queries, python, reader, add);
|
||||
// The next level: the Python importers that pass on what they import from these.
|
||||
if (depth > PYTHON_REEXPORT_DEPTH) break;
|
||||
// The next level: the importers that pass on what they import from these.
|
||||
const next: Module[] = [];
|
||||
for (const [importer, { language, statements }] of naming) {
|
||||
if (language !== 'python' || seen.has(importer)) continue;
|
||||
const passes = pythonPassedOn(importer, statements, reader);
|
||||
if (seen.has(importer) || depth > reExportDepth(language)) continue;
|
||||
const passes = passedOn(importer, language, statements, reader);
|
||||
if (passes === undefined) continue;
|
||||
seen.add(importer);
|
||||
next.push({ path: importer, language, passes });
|
||||
@@ -193,12 +195,14 @@ function importersOf(
|
||||
}
|
||||
|
||||
/**
|
||||
* What the Python module `importer` passes on of what its `statements`
|
||||
* import, or undefined when the resolver reads no name through it: what it
|
||||
* star-imports, and for a package's `__init__.py`, the names it imports too.
|
||||
* What `importer` passes on of what its `statements` import, or undefined
|
||||
* when the resolver reads no name through it: an ES module what it `export …
|
||||
* from`s; a Python module what it star-imports, and a package's `__init__.py`
|
||||
* the names it imports too.
|
||||
*/
|
||||
function pythonPassedOn(
|
||||
function passedOn(
|
||||
importer: string,
|
||||
language: Language,
|
||||
statements: ReadonlyArray<{ source: string; module: Module; onlyName?: string }>,
|
||||
reader: ImportReader,
|
||||
): Set<string> | null | undefined {
|
||||
@@ -206,15 +210,25 @@ function pythonPassedOn(
|
||||
const pass = (names: Set<string> | null): void => {
|
||||
passes = passes === null || names === null ? null : new Set([...(passes ?? []), ...names]);
|
||||
};
|
||||
const stars = new Set(reader.starImports(importer, 'python'));
|
||||
const passing = (module: Module, name: string): boolean => module.passes === null || module.passes.has(name);
|
||||
if (language !== 'python') {
|
||||
for (const rex of reader.reExports(importer, language)) {
|
||||
for (const { source, module } of statements) {
|
||||
if (source !== rex.source) continue;
|
||||
if (rex.kind === 'wildcard') pass(module.passes);
|
||||
else if (rex.kind === 'namespace') pass(new Set([rex.exportedName]));
|
||||
else if (passing(module, rex.originalName)) pass(new Set([rex.exportedName]));
|
||||
}
|
||||
}
|
||||
return passes;
|
||||
}
|
||||
const stars = new Set(reader.starImports(importer, language));
|
||||
const isPackage = importer === '__init__.py' || importer.endsWith('/__init__.py');
|
||||
for (const { source, module, onlyName } of statements) {
|
||||
if (onlyName === undefined && stars.has(source)) pass(module.passes);
|
||||
if (!isPackage) continue;
|
||||
const names = reader.mappings(importer, 'python')
|
||||
.filter((m) => m.source === source && (onlyName !== undefined
|
||||
? m.exportedName === onlyName
|
||||
: module.passes === null || module.passes.has(m.exportedName)))
|
||||
const names = reader.mappings(importer, language)
|
||||
.filter((m) => m.source === source && (onlyName !== undefined ? m.exportedName === onlyName : passing(module, m.exportedName)))
|
||||
.map((m) => m.localName);
|
||||
if (names.length > 0) pass(new Set(names));
|
||||
}
|
||||
@@ -288,7 +302,8 @@ function addPythonImporters(
|
||||
|
||||
/**
|
||||
* Per importer, what its statements naming the modules import: the modules
|
||||
* named whole (not the package around one), and the local names bound.
|
||||
* named whole (not the package around one), and the local names bound. An ES
|
||||
* namespace import (`import * as z`) reads whatever its module passes on.
|
||||
*/
|
||||
function bindingsOf(importers: Importers, reader: ImportReader): Map<string, { modules: string[]; names: string[] }> {
|
||||
const bindings = new Map<string, { modules: string[]; names: string[] }>();
|
||||
@@ -297,7 +312,9 @@ function bindingsOf(importers: Importers, reader: ImportReader): Map<string, { m
|
||||
reader.mappings(file, language)
|
||||
.filter((m) => {
|
||||
const only = sources.get(m.source);
|
||||
return only === null || (only !== undefined && only.has(m.exportedName));
|
||||
if (only === null) return true;
|
||||
if (only === undefined) return false;
|
||||
return only.has(m.exportedName) || (m.isNamespace && language !== 'python' && only.size > 0);
|
||||
})
|
||||
.map((m) => m.localName)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user