Repository navigation
fix(sync): imports through a re-exporting module follow it when it is deleted or added - #2512
Merged
Merged
Conversation
… deleted or added Deleting a module that re-exports what it imports (a package's `__init__.py`, an `index.ts` barrel) left every file importing through it linked to the symbols it re-exported: those edges point into files that stay, so the removal cascaded none of them. When such a module was added or came back, its importers kept the edges a name lookup had made in its absence, or stayed failed; and in a large Python project the failed import of the module itself stayed failed, because the failed-import retry skips a stem with more than 500 imports waiting (Django's `test`, `models`). Sync now re-opens, as pending references for its sweep, what the files importing a removed or added module bind through those imports: before the removed files are deleted, and after the retries for the added ones (with their failed references and the module's own failed import). Importers are found by the import edge into the file, by Python import nodes naming the module or its package, and through what the module passes on (an import inside a function has no node); in Python, a package or star import re-exporting a changed module leads on to its own importers, as deep as the resolver chases a name. All changed files are handled in one pass, with importer edges and failed references filtered by the bound names in SQL. Python and the ES-module family only; no schema or extraction change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 12, 2026
colbymchenry
added a commit
that referenced
this pull request
Oct 14, 2026
…2560) A lazy React Router route whose module is a barrel, and whose page sits in a folder below the barrel's, stayed unlinked after `codegraph sync` added the page or gave it its export, though a fresh index linked it. In DigiShop.Frontend, `@/pages` is `src/pages/index.ts`, which imports `./Home`, `src/pages/Home/index.tsx`, and exports it again. While the page is missing, the route's reference fails and waits under the key of the module it names, `module:pages` (#2422). The retry looks failed references up by the keys of the changed files, and a file in `pages/Home/` gives only `module:Home` and its own stem. The limit, which the `export *` barrels of #2436 share, was recorded with #2558. After the failed-ref retry, which makes the barrel's import edge into an added file, sync now walks up the import edges from each file it added, or gave a definition, to the barrels that pass it on: `export … from`, or an import exported again (`import { X } from './x'; export { X }`, `export default X`), as deep as the resolver chases a name. It re-opens, for the sweep, the failed references waiting under those barrels' keys. Only keys a failed reference waits under are looked up, in one DISTINCT over the failed rows parked under module keys, so a sync with none reads nothing, and a body-only edit seeds nothing. The ImportReader that the added-files reopen (#2512) uses is shared, and gains exportedBindings, behind a regex pre-check, since most importers export nothing. defaultExportBinding moves to alias-binding.ts for this walk and for react.ts's exportsBinding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
codegraph syncand the watcher never revisited the files that import through a module when that module was deleted or added. Here "through" means a module that re-exports what it imports: a package's__init__.py, anindex.tsbarrel, or a plain module that re-exports. A fresh index of the same tree disagreed in both directions:from shop import Order, whereshop/__init__.pyre-exportsOrderfromshop/models.py, stayed linked aftershop/__init__.pywas deleted. So didimport { Button } from './components'aftercomponents/index.tswas deleted. Those edges point into files that stay, so removing the barrel cascaded none of them. A fresh index of the tree without the barrel links them by name or not at all.exact-match) or failed. When the barrel appeared, nothing re-resolved them. They stayed name-linked where a fresh index links them through the barrel (import), or stayed failed. This is the "same edge, other metadata" residue fix(sync): a reference through an import binding links its module when the module appears later #2450, fix(sync): a Python import alias links its module when the module appears later #2501 and fix(sync): a Python import of a name a package re-exports links when the package appears later #2507 all listed.from django.test import TestCasestayed failed afterdjango/test/__init__.pycame back. The failed-import retry looks such an import up by the module's stem, and skips a stem with more than 500 failed imports waiting. In Django,testandmodelsare past that. This was fix(sync): a Python import of a name a package re-exports links when the package appears later #2507's listed residue "fix(sync): an import links the file it names when that file appears later #2392's import-retry ceiling (django.test)".from django.contrib.gis.db import modelsreachesdjango/db/models/__init__.pythrough the gis package, which star-imports it. Itsmodels.CharFieldstayed linked after the inner__init__.pywas deleted.Found while working on #2507: its restore-after-delete test couldn't pass, because deleting the barrel unlinked nothing.
Fix
src/resolution/module-importers.ts: when sync removes or adds files, it re-opens the references that the files importing them make through those imports, back to pending for the sync's sweep. The sweep binds them against the post-sync graph as a full index does: through the module when it is there, by name or not at all when it isn't. A failed one is parked under its module's key (#2501, #2507), so it relinks when the module comes back.refNameis a binding of a statement naming a removed file, or a member read through one (Order.objects,ns.fn). Edges into a removed file itself are left to the existing resurrection.getMaxEdgeIdmark), their failed references through those bindings, and the failed import of the module itself.importsedge into the file's node, for ES modules and dotted Python imports;from . import Order,from .. import Order,from shop import Order; the relative ones never resolve to a file);from django.db import models, thenmodels.Model);__init__.pyimporting names from it. Only the names it passes on are followed, three modules deep, as far as the resolver'spythonModuleSymbolchases a name.#includeor a Go package import binds no reference name of its own.THROUGH_NAMES). Each importer is visited once per sync, and each file is read once for its import statements.No schema change and no extraction change (so no kernel change). Fresh indexes are byte-identical to main's.
Validation
Tests.
__tests__/sync-module-importers.test.tshas 6 tests: a Python package and module, and an ES-module barrel, each deleted, deleted and restored, and added after its importers were indexed. Each compares the synced graph with a fresh index of the final tree in a second folder.from app import ormwithorm.Model, a function-local import, a star-importing package and a package re-exporting one name, plus 501 failed imports namedshop(over the retry's ceiling).__init__re-exports and the nested levels.CODEGRAPH_KERNEL=0too.A/B on real repos. Each arm's synced graph is compared with its own fresh index of the same tree in another folder. Cells are the differing graph lines, main → this PR.
__init__.pyorindex.{ts,tsx,js,jsx}(django 36, netbox 27, pydantic 2, fastapi 2, vben 114, vue3-element-admin 12, takenote 5).async_view, in Django's two every-7th restore and add scenarios. It is an existing difference between pooled and sequential resolution: this PR re-opens the reference, and the sweep's small batch resolves it sequentially, where a fresh index resolves it pooled.Cost. Sync wall time, main and this PR, interleaved over two rounds. The machine was shared, so a few runs are noisy (fastapi's main at 15.2 s).
__init__.pydeleted__init__.pydeleted__init__.pydeletedindex.tsdeletedindex.tsadded lateIn Django's 36-package deletion, the importer pass re-opens 29,501 references in 3.1 s: it finds the importers, then swaps their edges for references. The rest of the difference is the sweep re-resolving them, which a fresh index does too. For scale, a fresh index takes about 31 s for Django and 15 s for netbox. An earlier draft of this PR, which visited the importers once per removed file and filtered their edges in JS, took 6–11 s for the same pass.
Residues
index.tsthat re-exports another barrel (export * from './dropdown-menu') is not followed.export … fromleaves no import edge or node.export *and the specifier leave nothing, and a named re-export leaves only animportsreference for its original name. So an outer barrel can't be found from the index. It would take an extraction change (edges forexport … from, which changes every fresh index) or a directory scan.from django.test import TestCasenames a package outside the repo, so a fresh index linksTestCaseby name to netbox's own test class. The same goes forassertHttpStatus(801) andadd_permissions(576). When the defining file comes back, the retry looks these up by name, and each has more than 500 failed references waiting. This is the same on main and is deliberate (codegraph index wedges indefinitely at "Resolving refs" 94% on a repo with a large *tracked* JS/TS asset dir and there's no way to exclude it #999's rationale).obj.save()orinstance.refresh_from_db()on a test model whoseclass R(models.Model)base came throughdjango/db/models/__init__.py. Theextends models.Modelreference is re-opened and fails, as in a fresh index. The calls name a local variable, not an import, so they keep their edges. When the package is added late, the same calls stay failed, becausesaveis past the Incremental sync misses cross-file relations after a new export is added #1240 ceiling. Both are the same on main.router/index.tsis deleted, andNoteItem. Both are the same on main.__init__.pyfiles and star imports are).Overlap
src/resolution/import-resolver.ts, but only its re-export reader. This PR only exportspythonModulePathfrom that file.🤖 Generated with Claude Code