mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-22 09:35:22 +08:00
fix(sync): an import a barrel passes on under another name relinks when its module gains it (#2665)
A sync that gives a module a definition retries the failed references
waiting for the names the module declares, and rebinds the edges into the
names that changed (CG-33). An importer that reaches the module through a
barrel passing it on under another name waits under neither: matx's
`export { default as MatxLoading } from './MatxLoading'` hands
MatxLoading.jsx to `import { MatxLoading } from 'app/components'`, a name
the file never declares. That import and the calls through it stayed
failed, and where a name match had taken the name elsewhere while the
module lacked it (vben's `alert`, which its alert barrel passes on from
`vbenAlert`, onto the `alert.vue` component), the edge stayed there, until
the importer changed or the project was indexed again.
reopenReferencesThroughBarrels now records each importer below a barrel
with the names the barrel passes the changed files on as, and re-opens that
importer's failed references through those names and, for the names the
files never declare, its edges bound before the sync resolved anything, as
the added-file path does. A declared name stays the retry's and the
rebind's, which cap a common one. The per-importer lookup replaces the
repo-wide retry by the passed-on names, whose per-name ceiling skips a name
as common as `alert`; the barrels' module keys are still retried for lazy
routes. passedOn, which the added-file path shares, now also reads an
import a barrel exports again, with the names it exports it as (`import
Card from './Card'; export { Card as AppCard }`), which only the lazy-route
walk followed before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+12
-8
@@ -1129,13 +1129,17 @@ export class CodeGraph {
|
||||
if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[phase-timing] sync-added-module-importers: ${Date.now() - tAdded}ms (${reopened} refs re-opened)`);
|
||||
}
|
||||
|
||||
// A failed reference that waits for a module through a barrel —
|
||||
// React Router's `import:@/pages#HomePage`, parked under
|
||||
// `module:pages` — waits for a file below the barrel too
|
||||
// (`pages/Home/index.tsx`), whose own keys name its folder and
|
||||
// stem. Those parked under the keys of the barrels that pass on a
|
||||
// file this sync adds, or one whose definitions it changed, go
|
||||
// back to pending for the sweep; a body-only edit satisfies none.
|
||||
// A reference that reaches a module through a barrel waits for a
|
||||
// file below it: React Router's `import:@/pages#HomePage`, parked
|
||||
// under `module:pages`, for `pages/Home/index.tsx`, whose own keys
|
||||
// name its folder and stem; `import { MatxLoading } from
|
||||
// './components'` for the `MatxLoading.jsx` the barrel passes on
|
||||
// as `default as MatxLoading`, a name that file never declares.
|
||||
// Those that reach a file this sync adds, or one whose definitions
|
||||
// it changed, go back to pending for the sweep: the failed ones,
|
||||
// and the edges bound before this sync resolved anything, which a
|
||||
// name match may have taken elsewhere while the file lacked the
|
||||
// name. A body-only edit satisfies none.
|
||||
const tBarrels = Date.now();
|
||||
const delta = new Set(result.definitionDelta ?? []);
|
||||
const seeds = new Map<string, Language>(added.map((file) => [file.path, file.language]));
|
||||
@@ -1146,7 +1150,7 @@ export class CodeGraph {
|
||||
}
|
||||
}
|
||||
const throughBarrels = reopenReferencesThroughBarrels(
|
||||
this.queries, importReader, [...seeds].map(([path, language]) => ({ path, language }))
|
||||
this.queries, importReader, [...seeds].map(([path, language]) => ({ path, language })), edgesBeforeResolution
|
||||
);
|
||||
if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[phase-timing] sync-barrel-modules: ${Date.now() - tBarrels}ms (${throughBarrels} refs re-opened)`);
|
||||
} else {
|
||||
|
||||
@@ -81,15 +81,16 @@ export interface ImportReader {
|
||||
/** The modules a Python file star-imports, which have no import mapping. */
|
||||
starImports(file: string, language: Language): string[];
|
||||
/**
|
||||
* The bindings an ES module's own exports hand over as they are: each local
|
||||
* an export clause names (`export { Page }`), and its default export when
|
||||
* that is a binding (`export default Page;`).
|
||||
* The bindings an ES module's own exports hand over as they are, each with
|
||||
* the names it is exported as: each local an export clause names (`export {
|
||||
* Page }`, `export { Card as AppCard }`), and its default export when that
|
||||
* is a binding (`export default Page;`, exported as `default`).
|
||||
*/
|
||||
exportedBindings(file: string, language: Language): ReadonlySet<string>;
|
||||
exportedAs(file: string, language: Language): ReadonlyMap<string, readonly string[]>;
|
||||
}
|
||||
|
||||
export function createImportReader(rootDir: string): ImportReader {
|
||||
const memo = new Map<string, { content: string; mappings: ImportMapping[]; reExports: ReExport[]; starImports: string[]; exported?: ReadonlySet<string> }>();
|
||||
const memo = new Map<string, { content: string; mappings: ImportMapping[]; reExports: ReExport[]; starImports: string[]; exported?: Map<string, string[]> }>();
|
||||
const read = (file: string, language: Language) => {
|
||||
let entry = memo.get(file);
|
||||
if (!entry) {
|
||||
@@ -109,25 +110,31 @@ export function createImportReader(rootDir: string): ImportReader {
|
||||
}
|
||||
return entry;
|
||||
};
|
||||
const exportsOf = (file: string, language: Language): Map<string, string[]> => {
|
||||
const entry = read(file, language);
|
||||
if (!entry.exported) {
|
||||
// Most importers export nothing they import; a commented-out clause
|
||||
// read as one only costs a key lookup.
|
||||
const exported = new Map<string, string[]>();
|
||||
const add = (local: string, as: string): void => {
|
||||
const names = exported.get(local);
|
||||
if (names) names.push(as);
|
||||
else exported.set(local, [as]);
|
||||
};
|
||||
if (EXPORTS_A_BINDING.test(entry.content)) {
|
||||
for (const alias of extractLocalExportAliases(entry.content)) add(alias.localName, alias.exportedName);
|
||||
const binding = defaultExportBinding(entry.content);
|
||||
if (binding) add(binding, 'default');
|
||||
}
|
||||
entry.exported = exported;
|
||||
}
|
||||
return entry.exported;
|
||||
};
|
||||
return {
|
||||
mappings: (file, language) => read(file, language).mappings,
|
||||
reExports: (file, language) => read(file, language).reExports,
|
||||
starImports: (file, language) => read(file, language).starImports,
|
||||
exportedBindings: (file, language) => {
|
||||
const entry = read(file, language);
|
||||
if (!entry.exported) {
|
||||
// Most importers export nothing they import; a commented-out clause
|
||||
// read as one only costs a key lookup.
|
||||
const exported = new Set<string>();
|
||||
if (EXPORTS_A_BINDING.test(entry.content)) {
|
||||
for (const alias of extractLocalExportAliases(entry.content)) exported.add(alias.localName);
|
||||
const binding = defaultExportBinding(entry.content);
|
||||
if (binding) exported.add(binding);
|
||||
}
|
||||
entry.exported = exported;
|
||||
}
|
||||
return entry.exported;
|
||||
},
|
||||
exportedAs: (file, language) => exportsOf(file, language),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,60 +190,85 @@ export function reopenReferencesThroughAddedFiles(
|
||||
const BARREL_LANGUAGES = new Set<Language>(['typescript', 'tsx', 'javascript', 'jsx']);
|
||||
|
||||
/**
|
||||
* Re-open, for the sync's sweep, the failed references that wait for a module
|
||||
* through a barrel when a file below the barrel appears or gains a
|
||||
* definition. React Router's `import:@/pages#HomePage` waits under the key of
|
||||
* the module it names, `module:pages`. When `pages/index.ts` passes on
|
||||
* Re-open, for the sync's sweep, the references that reach a module through a
|
||||
* barrel when a file below the barrel appears or gains a definition. React
|
||||
* Router's `import:@/pages#HomePage` waits under the key of the module it
|
||||
* names, `module:pages`. When `pages/index.ts` passes on
|
||||
* `pages/Home/index.tsx`, that file is what the reference waits for, but its
|
||||
* own keys name its folder and stem (`module:Home`), never the barrel's.
|
||||
*
|
||||
* The barrels are the modules that pass one of `files` on, through `export …
|
||||
* from` or an import they export again (`import { HomePage } from './Home';
|
||||
* export { HomePage }`), followed up as deep as the resolver chases a name.
|
||||
* Only keys a failed reference waits under are looked up, so a sync with none
|
||||
* reads nothing. Call after the failed-ref retry: an added file's importers
|
||||
* reach it through the import edges that retry made. Returns the number
|
||||
* re-opened.
|
||||
* export { HomePage }`), followed up as deep as the resolver chases a name
|
||||
* ({@link passedOn}). What waits under their keys goes back, and so does what
|
||||
* their importers reach through a name they pass the file on as: matx's
|
||||
* `export { default as MatxLoading } from './MatxLoading'` hands
|
||||
* `MatxLoading.jsx` to `import { MatxLoading } from 'app/components'`, a name
|
||||
* the file never declares, so neither the retry by the file's names nor the
|
||||
* rebind of the edges into them reaches it. Such an importer's failed
|
||||
* references through those names go back. So do its edges through the names
|
||||
* the files never declare, bound before this sync resolved anything (up to
|
||||
* `resolvedUpToEdgeId`): while the module lacked the binding, a name match
|
||||
* could take the name elsewhere (vben's `alert`, which the barrel passes on
|
||||
* from `vbenAlert`, to the `alert.vue` component). A name a file declares is
|
||||
* the rebind's, which caps a common one, and a chain of `export *` hands over
|
||||
* only those. Call after the failed-ref retry: an added file's importers
|
||||
* reach it through the import edges it made. Returns the number re-opened.
|
||||
*/
|
||||
export function reopenReferencesThroughBarrels(
|
||||
queries: QueryBuilder,
|
||||
reader: ImportReader,
|
||||
files: ReadonlyArray<{ path: string; language: Language }>,
|
||||
resolvedUpToEdgeId: number,
|
||||
): number {
|
||||
let level = files.filter((file) => BARREL_LANGUAGES.has(file.language));
|
||||
let level: Module[] = files.filter((file) => BARREL_LANGUAGES.has(file.language)).map((file) => ({ ...file, passes: null }));
|
||||
if (level.length === 0) return 0;
|
||||
const waiting = new Set(queries.getFailedModuleKeys());
|
||||
if (waiting.size === 0) return 0;
|
||||
const keys = new Set<string>();
|
||||
const seen = new Set(level.map((file) => file.path));
|
||||
for (let depth = 1; level.length > 0 && depth <= REEXPORT_MAX_DEPTH; depth++) {
|
||||
const next: Array<{ path: string; language: Language }> = [];
|
||||
const declared = new Set(queries.getNodesByFiles(level.map((module) => module.path)).map((node) => node.name));
|
||||
// The importers below a barrel, with the names it passes the files on as,
|
||||
// and with those of them the files never declare.
|
||||
const importers: Importers = new Map();
|
||||
const renamed: Importers = new Map();
|
||||
const record = (into: Importers, importer: string, language: Language, source: string, names: string[]): void => {
|
||||
if (names.length === 0) return;
|
||||
const entry = into.get(importer) ?? { language, sources: new Map<string, Set<string> | null>() };
|
||||
entry.sources.set(source, new Set([...(entry.sources.get(source) ?? []), ...names]));
|
||||
into.set(importer, entry);
|
||||
};
|
||||
const seen = new Set(level.map((module) => module.path));
|
||||
for (let depth = 1; level.length > 0; depth++) {
|
||||
// Per importer of this level, its statements naming each module of it.
|
||||
const naming = new Map<string, { language: Language; statements: Array<{ source: string; module: Module }> }>();
|
||||
for (const module of level) {
|
||||
for (const i of queries.getImportsInto(`file:${module.path}`, module.path)) {
|
||||
if (i.refName === null || seen.has(i.filePath) || !BARREL_LANGUAGES.has(i.language)) continue;
|
||||
if (!passesOn(i.filePath, i.language, i.refName, reader)) continue;
|
||||
seen.add(i.filePath);
|
||||
for (const key of moduleReferenceKeys(i.filePath)) if (waiting.has(key)) keys.add(key);
|
||||
next.push({ path: i.filePath, language: i.language });
|
||||
if (i.refName === null || seen.has(i.filePath)) continue;
|
||||
if (module.passes !== null && i.language !== 'python' && BINDING_LANGUAGES.has(i.language)) {
|
||||
record(importers, i.filePath, i.language, i.refName, [...module.passes]);
|
||||
record(renamed, i.filePath, i.language, i.refName, [...module.passes].filter((name) => !declared.has(name)));
|
||||
}
|
||||
if (depth > REEXPORT_MAX_DEPTH || !BARREL_LANGUAGES.has(i.language)) continue;
|
||||
const found = naming.get(i.filePath) ?? { language: i.language, statements: [] };
|
||||
found.statements.push({ source: i.refName, module });
|
||||
naming.set(i.filePath, found);
|
||||
}
|
||||
}
|
||||
const next: Module[] = [];
|
||||
for (const [importer, { language, statements }] of naming) {
|
||||
const passes = passedOn(importer, language, statements, reader);
|
||||
if (passes === undefined) continue;
|
||||
seen.add(importer);
|
||||
for (const key of moduleReferenceKeys(importer)) if (waiting.has(key)) keys.add(key);
|
||||
next.push({ path: importer, language, passes });
|
||||
}
|
||||
level = next;
|
||||
}
|
||||
if (keys.size === 0) return 0;
|
||||
const parked = queries.getRetryableFailedReferences([...keys]).flatMap((ref) => (ref.rowId === undefined ? [] : [ref.rowId]));
|
||||
return queries.reopenFailedReferences(parked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `importer` passes on what it imports from `source`: an `export …
|
||||
* from` of it, or a binding it imports from it and exports again — the forms
|
||||
* a lazy route's lookup follows through a barrel (`exportedComponent`).
|
||||
*/
|
||||
function passesOn(importer: string, language: Language, source: string, reader: ImportReader): boolean {
|
||||
if (reader.reExports(importer, language).some((rex) => rex.source === source)) return true;
|
||||
const exported = reader.exportedBindings(importer, language);
|
||||
return exported.size > 0 &&
|
||||
reader.mappings(importer, language).some((m) => m.source === source && !m.isNamespace && exported.has(m.localName));
|
||||
const failed = new Set([...bindingsOf(importers, reader)].flatMap(([file, { names }]) => queries.getFailedReferencesThrough(file, [], names)));
|
||||
if (keys.size > 0) {
|
||||
for (const ref of queries.getRetryableFailedReferences([...keys])) if (ref.rowId !== undefined) failed.add(ref.rowId);
|
||||
}
|
||||
return reopenEdges(queries, bindingsOf(renamed, reader), (e) => e.edgeId <= resolvedUpToEdgeId) +
|
||||
queries.reopenFailedReferences([...failed]);
|
||||
}
|
||||
|
||||
/** An import's module edge into a file that an added file now outranks. */
|
||||
@@ -357,10 +389,12 @@ function importersOf(
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* What `importer` passes on of what its `statements` import, under the names
|
||||
* its own importers use, or undefined when the resolver reads no name through
|
||||
* it: an ES module what it `export … from`s and the imports it exports again
|
||||
* (`import Card from './Card'; export { Card as AppCard }`); a Python module
|
||||
* what it star-imports, and a package's `__init__.py` the names it imports
|
||||
* too.
|
||||
*/
|
||||
function passedOn(
|
||||
importer: string,
|
||||
@@ -382,6 +416,16 @@ function passedOn(
|
||||
else if (passing(module, rex.originalName)) pass(new Set([rex.exportedName]));
|
||||
}
|
||||
}
|
||||
const exportedAs = reader.exportedAs(importer, language);
|
||||
if (exportedAs.size > 0) {
|
||||
for (const m of reader.mappings(importer, language)) {
|
||||
const names = m.isNamespace ? undefined : exportedAs.get(m.localName);
|
||||
if (!names) continue;
|
||||
for (const { source, module } of statements) {
|
||||
if (source === m.source && passing(module, m.isDefault ? 'default' : m.exportedName)) pass(new Set(names));
|
||||
}
|
||||
}
|
||||
}
|
||||
return passes;
|
||||
}
|
||||
const stars = new Set(reader.starImports(importer, language));
|
||||
|
||||
Reference in New Issue
Block a user