Skip to content

fix(sync): a member reference through an import binding links its module when the module changes - #2630

Merged
colbymchenry merged 1 commit into
mainfrom
claude/elated-morse-1af6fd
Oct 17, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/elated-morse-1af6fd

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

A member reference through an import binding is linked by a fresh index through the import (calls, resolvedBy: import):

  • NODE_PATH_EXCLUDELIST.includes(p) after import { NODE_PATH_EXCLUDELIST } from './constants', to the constant;
  • store.get(k) through a default import, to the store;
  • useStore.setState() through an aliased import;
  • api.get() through a namespace import, to the module's export.

When the module was missing, or lacked the export, when the importing file was indexed, the reference failed. It was parked under its tail (#1240), which for a member is the member's name: includes, get, setState, map. A sync that later restored the module or gave it the export never relinked it:

  • the module seldom declares a symbol named includes or get, so the name lookup never matched;
  • and getRetryableFailedReferences skips any name with more than 500 failed references. On real repos, includes, map, get, forEach and setState all have that many.

Evidence (2026-10-07, on main ed199e6 and #2450): every 7th source file was held out or emptied, the rest indexed, the files restored, synced, and compared with a fresh index. That left 194 such residues across 45 runs. Examples: infrahub NODE_PATH_EXCLUDELIST.includes and store.set (63), ghostfolio SCOPES_OF_WRITE_ACCESS.includes, milo-ui REGISTERED_FLAGS.get and COLUMNS.map, folo useEntryStore.setState (19).

Re-checked on current main (3be906f)

Main has changed since then. #2512 now re-opens an importer's failed references through the bindings of an added module, member reads included, so the held-out runs above are fixed on main. What is left, measured with this PR's tests against main's source:

  • an edit that gives an existing module its export (the emptied-module runs, most of the residues above): 7 of 7 member links stay unlinked;
  • an edit that gives the export to a file a barrel forwards (export * from './shared/flags'): unlinked;
  • a module added later or restored in a project that uses Vue, imported through a tsconfig alias (@/flags/registry, a SFC's @/table): 2 of 7 stay unlinked. Vue's resolver links the import's module specifier to the constant the file declares, not to the file. fix(sync): imports through a re-exporting module follow it when it is deleted or added #2512 finds importers through imports edges into the file node, so it never finds this one.

Fix

A direct member reference (R.m) through an import binding of a project module is parked under the module's key, the way #2450 parks a whole name through a binding (importBindingTail, ES-module family; Python is unchanged). #2450 showed that re-keying a parked reference must keep every trigger it had, so it now waits for three names:

trigger why it matters how sync finds it
its module (module:constants) the file that appears or gains the export name_tail IN (moduleReferenceKeys), the existing lookup under #2507's module-key ceiling; #2560's barrel walk also re-opens it under a barrel's key
its member (includes, make) a namespace import's barrel forwards the member from a file of another name; a guess by name (instance-method) links a receiver whose module never resolves new idx_unresolved_failed_module_member (schema v16), an expression index on the tail the name gives. It shares one ceiling with the references still under that tail, so main's decision for every member name is unchanged
its receiver (FLAGS of FLAGS.includes) a barrel's import of the declaring file that no walk finds (the Vue case above) new prefix range FLAGS. to FLAGS/ on #2450's idx_unresolved_failed_module_name

The two other readers of a failed call's tail still find these:

  • the router navigation retry (getFailedCallsByTail): router.push parked under module:router;
  • the dead-code check (getUnresolvedNamesAmong).

Both read module-parked rows by member through the new index.

Not chains or longer paths. URI.file(p).toString() and vscode.window.showErrorMessage keep their member's tail. Their last member hangs off what another member returns or holds, seldom a type the module declares. Parking them too tripled the refs an edit of vscode's uri.ts retries (+2,497 refs, +363 ms), and none of them resolved. Across 19 repos the only cost of leaving them out is two .vue chains in vue3-element-admin.

Rows parked before v16 keep their member tails, which the tail lookup still finds; a re-index parks them under their modules. The v16 migration only creates the partial index (155 ms on vscode's index).

Validation

Tests. __tests__/sync-import-member-retry.test.ts has 16 tests. Each integration test compares the synced graph with a fresh index of the final tree in a second folder.

A/B on the 19-repo kit from #2450, main 3be906f against this PR, with the same kernel. Each arm's synced graph is compared with its own fresh index. Cells are differing graph lines.

emptied every 7th file, then restored main this PR what went
infrahub 146 0 store.set ×63, store.get ×5, NODE_PATH_EXCLUDELIST / OBJECT_PERMISSION_* .includes
milo-ui 924 854 CRYSTAL_BALL_ROUTES.DASHBOARD_DETAIL ×8, HISTORY_TIME_RANGE_OPTIONS.*, *_ERROR_CODES.includes, mocked hooks (useTasks.mockReturnValue …)
folo 216 198 i18n.t, kv.get, useUserStore.setState, PROVIDER_OPTIONS.filter, …
ghostfolio 80 72 SCOPES_OF_WRITE_ACCESS.includes
vue-realworld 10 4 a navigation call through an imported router
vue3-element-admin, vue-element-admin 182, 18 182, 18 —
10 other JS repos 0 0 —

Cost on vscode's index (816K failed refs; the shared copy opened read-only, lookups on a private copy; interleaved rounds, medians):

main this PR
parking decisions, per full index 919 ms 1,296 ms
references parked under a module key 1,236 6,333 (5,097 member refs; 3 keys over 500, none over 5,000)
retry lookup: uri.ts / 10 files / 100 / 1,000 changed TS files 5.4 / 15.5 / 377 / 2,807 ms 7.0 / 19.4 / 438 / 3,023 ms
refs re-resolved: uri.ts / lifecycle.ts / dom.ts edit 1,219 / 1,877 / 4,423 1,226 / 1,883 / 5,060
their resolution: uri.ts / dom.ts / 100 files 499 / 2,910 / 7,084 ms 499 / 3,104 / 7,197 ms
navigation retry lookup, dead-code names (5,421) 60 ms, 2,720 ms 64 ms, 2,649 ms
failed module keys (#2560's barrel pre-check) 1.3 ms 4.7 ms

dom.ts retries 637 more refs: vscode's dom.* calls go through an import whose module never resolves, so they now wait under module:dom. That is the trigger this PR adds, and an edit of dom.ts is what can resolve them.

Notes

🤖 Generated with Claude Code

…ule when the module changes

`NODE_PATH_EXCLUDELIST.includes(p)` after `import { NODE_PATH_EXCLUDELIST }
from './constants'`, `store.get(k)` through a default import or
`api.get()` through a namespace import is linked by a fresh index through
the import. When the module was missing or lacked the export, the failed
reference was parked under its member's name (`includes`, `get`), which the
module seldom declares and which the retry skips past its 500 ceiling. A
sync that gave the module the export never relinked it, nor one that added
the module where no import edge reached its file.

A direct member reference through an import binding of a project module is
now parked under the module's key, as whole names through a binding are
since #2450, and keeps two more triggers. Its member's name, through a new
expression index on the tail its name gives (schema v16), under the same
ceiling as the references still parked under that name: a namespace
import's barrel forwards the member from another file, and a guess by name
links a receiver whose module never resolves. Its receiver's name, through
a prefix range on the module-name index of #2450: a barrel's import of the
declaring file that no walk finds. The router navigation retry and the
dead-code check read module-parked calls by their member through the new
index. A chain or a longer path keeps its member's tail: on vscode,
parking them too tripled the refs an edit of a hot module retries, and
none of them resolved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant