Repository navigation
fix(sync): a member reference through an import binding links its module when the module changes - #2630
Merged
Conversation
…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>
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
A member reference through an import binding is linked by a fresh index through the import (
calls,resolvedBy: import):NODE_PATH_EXCLUDELIST.includes(p)afterimport { 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:includesorget, so the name lookup never matched;getRetryableFailedReferencesskips any name with more than 500 failed references. On real repos,includes,map,get,forEachandsetStateall 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.includesandstore.set(63), ghostfolioSCOPES_OF_WRITE_ACCESS.includes, milo-uiREGISTERED_FLAGS.getandCOLUMNS.map, folouseEntryStore.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:
export * from './shared/flags'): unlinked;tsconfigalias (@/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 throughimportsedges 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:module:constants)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 keyincludes,make)instance-method) links a receiver whose module never resolvesidx_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 unchangedFLAGSofFLAGS.includes)FLAGS.toFLAGS/on #2450'sidx_unresolved_failed_module_nameThe two other readers of a failed call's tail still find these:
getFailedCallsByTail):router.pushparked undermodule:router;getUnresolvedNamesAmong).Both read module-parked rows by member through the new index.
Not chains or longer paths.
URI.file(p).toString()andvscode.window.showErrorMessagekeep 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'suri.tsretries (+2,497 refs, +363 ms), and none of them resolved. Across 19 repos the only cost of leaving them out is two.vuechains 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.tshas 16 tests. Each integration test compares the synced graph with a fresh index of the final tree in a second folder.router.pushrelinked by the navigation retry. Each asserts the module-parked tail first, which is where they fail on main; without that line they pass on main too.MEMBER_TAIL_SQLdefines.sync-import-binding-retry.test.ts(fix(sync): a reference through an import binding links its module when the module appears later #2450) now expectsNS.makeundermodule:ns: a namespace member waits for its module too, and its member name is still a key.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.
store.set×63,store.get×5,NODE_PATH_EXCLUDELIST/OBJECT_PERMISSION_*.includesCRYSTAL_BALL_ROUTES.DASHBOARD_DETAIL×8,HISTORY_TIME_RANGE_OPTIONS.*,*_ERROR_CODES.includes, mocked hooks (useTasks.mockReturnValue…)i18n.t,kv.get,useUserStore.setState,PROVIDER_OPTIONS.filter, …SCOPES_OF_WRITE_ACCESS.includescodegraph syncwithFOREIGN KEY constraint failedon both arms in all three scenarios: that iscodegraph syncfails with "FOREIGN KEY constraint failed" after d0bb9be5 when a file lazy-loads a workspace package #2565, with fix PR fix(sync): record only indexed files as synthesis inputs (#2565) #2574 open.const cache = new LRUCache(); cache.get(k)).unresolved_refsdiffer only inname_tail, every onecalls failed: <member> -> module:*.Cost on vscode's index (816K failed refs; the shared copy opened read-only, lookups on a private copy; interleaved rounds, medians):
uri.ts/ 10 files / 100 / 1,000 changed TS filesuri.ts/lifecycle.ts/dom.tsedituri.ts/dom.ts/ 100 filesdom.tsretries 637 more refs: vscode'sdom.*calls go through an import whose module never resolves, so they now wait undermodule:dom. That is the trigger this PR adds, and an edit ofdom.tsis what can resolve them.Notes
importsref named by a module path (@/flags/registry) to a symbol of the module rather than its file node (resolvedBy: framework). That hides the importer from fix(sync): imports through a re-exporting module follow it when it is deleted or added #2512's and fix(sync): a lazy route relinks a page that appears below its barrel #2560's walks, which follow edges into file nodes. This PR doesn't depend on that edge. The resolver behavior itself could be a separate fix.cache.getabove); Python member calls through aliases.🤖 Generated with Claude Code