Skip to content

fix(rust): calls on field chains, struct literals and outside-type paths link by type - #2831

Merged
colbymchenry merged 1 commit into
mainfrom
claude/issue-2614-rust-receivers
Oct 23, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/issue-2614-rust-receivers

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

Issue #2614 reported four Rust call shapes that linked to a same-named project method. Case 1 (names.contains(&k) on a slice) was already fixed on main by the typed-receiver work (#2715). This PR fixes the other three, which still reproduced on main 5b47bc4 in both engines:

case call main this PR
2 vis.map.get("a"), map: HashMap<String, i32> Ext::get (exact-match 0.9) no edge
3 J::from(i) under use serde_json::Value as J Fail::from (instance-method 0.7) no edge
4 Xyz { x: 1.0 }.to_val() Stat::to_val (exact-match 0.4) Xyz::to_val (instance-method 0.9)

Stat { n: 0.0 }.to_val() in the issue's main keeps its target, now resolved by type (0.9) instead of a 0.4 name guess. Kernel and wasm give identical results on the issue's crate.

Root cause

  • Cases 2 and 4. The extractor keeps one receiver level on a call (x.m, self.m, self.field.m). A deeper field chain (vis.map.get, self.a.b.m, self.0.m) or a struct literal (Xyz { … }.to_val()) reaches the resolver as the bare method name. The typed-receiver reading from fix(rust): a call on a local reaches the method of the type it is declared with #2715–fix(rust): a call on a field of self reaches the method of the field's declared type #2719 only ran for ident.method and self.field.method refs, so these went to the name strategies: get had one project method (Ext::get, an associated fn at that), and to_val had two trait impls, picked by proximity.
  • Case 3. J::from matched no qualified name. J names no project type, so Strategy 1 found nothing, and a single capital letter slips past namesExternalType, which reads it as a possible generic parameter. Strategy 3 then took the crate's one from. The same family: under an unrenamed use serde_json::Value, Value::default() linked by qualified name (0.95) to a project Value's impl Default.

Fix

  • matchRustWrittenReceiverCall, an exclusive hook beside matchRustAsMutChainCall. For a bare-named Rust call, it reads the call's receiver from the text at the ref's column.

    • Only two receiver shapes are read: a field chain rooted at a local, a parameter or self, and a struct literal (alone or in parentheses).
    • The receiver is typed with the existing rustValueType, and the call is resolved with rustTypedMethodCall, as a typed local's call is.
    • These are left to the name strategies as before:
      • any other receiver (a call's value, an index, a macro);
      • a receiver whose type can't be read;
      • a type no node names, such as one a macro declares (tokio's type AtomicUnsignedShort = … inside cfg_has_atomic_u64! { });
      • a later link of the same chain that calls the same name again (m.a.get(k).get(j)), since the two refs share a position.
    • When the type has no such method, a provided method of a project trait that the type #[derive]s, and that is in scope, counts. The derive writes the impl, which the index doesn't hold (axum-extra's #[derive(TypedPath)] → with_query_params).
  • rustFieldValue now also reads an element of a tuple-typed field (self.pair.0 on pair: (Config, u8)), using the existing rustTupleField.

  • isRustPathCallReachable, in gateRustScope beside isRustForeignImplCallable. A call through a type path whose type the file's use, or the written path, names from std or a dependency reaches only:

    • the project's own impl of a trait for that type (impl Blank for serde_json::Value, impl From<Fail> for PathBuf), or
    • for a trait's path (W::flush(s) under use std::io::Write as W), the project's impls of that trait.

    A path dependency the project holds counts as the project's.

This is a resolver-only change: extraction is unchanged, so the kernel needs no mirror (see parity below).

Validation

Tests. The new __tests__/rust-written-receivers.test.ts (12 tests) covers the issue's crate verbatim and field-chain, struct-literal, derive and path variants. It also has guards for what must keep resolving: the project's impl From<Fail> for PathBuf, impl Blank for serde_json::Value, and W::flush(s) under use std::io::Write as W.

  • Red→green: with main's name-matcher.ts and index.ts swapped back in, 9 of the 12 fail (the 3 guards pass on both); with the fix, all 12 pass.
  • The 27 Rust test files, resolution and cross-language-resolution pass (416 tests).
  • Full suite on this commit (98a4337, kernel rebuilt from it, CODEGRAPH_KERNEL_EXPECT=1): 739 of 746 files passed and 6 were skipped; 8,844 tests passed and 82 were skipped. The one failure was daemon-lock-holder.test.ts › "reads a running process from the OS", which reads the OS process table and passed 11/11 when rerun alone.

A/B against main 5b47bc4. Both arms were built from that sha with the native kernel on, and calls edges were diffed by call site. Each change was triaged by reading the receiver's declaration (or, for a path, the file's use):

repo (commit) .rs files wrong links removed right links added wrong → right same target, now by type right links lost
ripgrep (3fce3b5b) 110 30 18 10 37 0
tokio (17638a0d) 814 281 58 54 141 0
axum (86aa6153) 302 18 7 0 7 0
rust-analyzer (0c4d7468), parser test data excluded 951 737 186 (+2 wrong, below) 434 843 0

How each repo was checked:

  • On ripgrep, tokio and axum, every changed site was read.
  • On rust-analyzer, all 289 path removals were checked against their file's use. For the 448 bare removals, all 99 sites in small groups were read, plus one sample from each of the 45 larger groups (349 sites).
  • Every group of rust-analyzer's additions (70) and retargets (124) was sampled.
  • Nodes are identical between arms on every repo, since extraction is unchanged.
  • Controls: gin (Go, 99 files) and express (JS, 141 files) have byte-identical full natural-key dumps.

What moved:

  • Removed: std or dependency calls that had landed on project namesakes.
    • File::open → MmapChoice::open, and a HashMap field's get → ContextModeLimited::get.
    • tokio test and bench files that use std::sync::Mutex / std::sync::atomic::AtomicUsize / std::time::Instant had been linked to tokio's own loom shims and time::Instant; std atomic fields went to loom's mutex-backed AtomicU64::store.
    • rust-analyzer's real std::ops::ControlFlow uses had gone to the fake core in test-utils/src/minicore.rs, and lsp_types::Position::new to a project Position.
    • tungstenite's Message::Text had gone to axum's own.
  • Added / retargeted: field chains now reach their declared type. Examples:
    • args.mode.update(…) → Mode::update, and tokio's split halves' self.0.peer_addr() → TcpStream::peer_addr;
    • 20 with_mut calls on loom UnsafeCell fields, which had gone to AtomicUsize::with_mut;
    • rust-analyzer's snap.analysis.hover(…), which had gone to Config::hover;
    • struct literals such as ParamEnvAndCrate { … }.store() and CommentKind { … }.prefix().
  • Two wrong links added (rust-analyzer): d.message.clone() on a message: String field links to syntax's ast::String::clone. The struct's file has const _: () = { use syntax::ast::*; … }, and the existing glob lookup (rustGlobItems, from fix(rust): a name a glob use brings in is an item the globbed module passes on #2714) reads that block-local glob as file-wide. Typed locals in such a file already misread String the same way on main. Making glob scope position-aware is a change to shared lookup, so I left it for a follow-up rather than widen this PR.
  • Two refinements came out of this triage:
    • An earlier draft ruled out calls on a field typed by a macro-declared alias, losing 5 right unsync_load links on tokio. A type no node names is now left to the name strategies.
    • The derive rule recovers axum-extra's UsersShow { … }.with_query_params(…) and adds Named { id: 1 }.to_uri().

Kernel parity (Rust): scripts/kernel-parity.mjs on ripgrep, tokio and axum gives 1226/1226 files at byte-parity, 0 deferred.

Cost. Interleaved, median of 3: ripgrep 6.7 s → 6.5 s, tokio 20.2 s → 20.8 s. The box was under heavy external load. A CPU profile of tokio puts about 0.27 s of 57 s busy CPU in the new functions and the shared typed reading they call; the rest of the run-to-run difference was spread evenly over code this change doesn't touch.

Rebase. The branch was rebased onto dcb985f (#2827, Ruby only). Re-indexing ripgrep, tokio, axum and rust-analyzer with the rebased build gives edge dumps identical to the pre-rebase fix arm.

Fixes #2614

Contributor PR #2625 (@danusha2345) was read for guidance: its struct-literal reading and its use-alias check for outside types shaped this fix. This PR builds on main's later typed-receiver reading instead of a separate declaration reader.

Left out (none are in the issue):

  • A tuple-struct construction as a receiver (Wrapper(x).m()), and a call on a method call's value inside a chain (a.b().m()), still go to the name strategies.
  • A renamed use of a project type (use crate::config::Config as Cfg; Cfg::load()) is unchanged (unresolved, as on main).
  • Two gaps in the shared typed reading surfaced during triage, and are left for follow-ups:
    • the block-scoped glob above;
    • a trait impl written for a type alias (impl HirDisplayWithExpressionStore for TypeRefId, type TypeRefId = Idx<TypeRef>), which isn't found through the alias. Three rust-analyzer hir_fmt calls there now link nothing; they previously linked to unrelated types.

🤖 Generated with Claude Code

…ths link by type

Issue #2614 reported four Rust calls that linked to a same-named project
method. Case 1 (a slice's contains) was fixed on main by the typed-receiver
work. The other three still reproduced on main in both engines:
vis.map.get("a") on a HashMap field went to Ext::get, J::from(i) under
`use serde_json::Value as J` to a local `impl From<io::Error> for Fail`, and
Xyz { x: 1.0 }.to_val() to Stat::to_val.

Root cause. The extractor keeps one receiver level on a call, so a deeper
field chain (vis.map.get, self.a.b.m, self.0.m) or a struct literal reaches
the resolver as the bare method name, and the typed-receiver reading only
ran for x.m and self.field.m refs. The name strategies then took the one
project get, or one of the two to_val impls. J::from matched no qualified
name, J names no project type, and a single capital letter is read as a
possible generic parameter, so Strategy 3 took the crate's one from. The
same family: under an unrenamed `use serde_json::Value`, Value::default()
linked by qualified name to a project Value's impl.

Fix.
- matchRustWrittenReceiverCall, an exclusive hook beside the as_mut one:
  for a bare-named call, read the receiver from the text at the ref's
  column. Only a field chain rooted at a local, a parameter or self, or a
  struct literal (alone or in parentheses) is read; it is typed with
  rustValueType and the call resolved with rustTypedMethodCall. Any other
  receiver, an unreadable one, or a type no node names (one a macro
  declares) is left to the name strategies. A provided method of a project
  trait the type derives counts (axum-extra's #[derive(TypedPath)]).
- rustFieldValue reads an element of a tuple-typed field (self.pair.0).
- isRustPathCallReachable in gateRustScope: a call through a type path
  whose type the file's use, or the written path, names from std or a
  dependency reaches only the project's impl of a trait for that type, or
  for a trait's path the project's impls of that trait. A path dependency
  the project holds is the project's.

Validation.
- New __tests__/rust-written-receivers.test.ts (12 tests: the issue's
  crate verbatim, chain/literal/derive/path variants, guards): 9 fail on
  main's sources, all pass with the fix. The 27 Rust test files plus
  resolution and cross-language resolution pass.
- A/B against main 5b47bc4 (kernel on), changed call sites triaged as
  removed wrong / added right / wrong to right / same target now by type:
  ripgrep 30 / 18 / 10 / 37 and tokio 281 / 58 / 54 / 141 (every site
  read), axum 18 / 7 / 0 / 7 (every site read), rust-analyzer without
  its parser test data (951 files) 737 / 186 / 434 / 843 (every path
  removal checked by its use; every group sampled). No right link lost.
  Two wrong links added on rust-analyzer: a block-scoped glob
  (`const _: () = { use syntax::ast::*; }`) is read file-wide by the
  existing glob lookup, so a `String` field there reads as ast::String;
  typed locals already misread it the same way on main. Nodes identical
  on every repo; controls gin and express byte-identical.
- Kernel parity (Rust): 1226/1226 files on ripgrep, tokio and axum.
  Resolver-only change, so the kernel needs no mirror.
- Cost: ripgrep median 6.7 s to 6.5 s, tokio 20.2 s to 20.8 s (interleaved,
  median of 3, noisy box); a CPU profile puts about 0.3 s of tokio's 57 s
  busy CPU in the new code and the typed reading it calls.

Fixes #2614

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 8fdc8a1 into main Oct 23, 2026
@colbymchenry
colbymchenry deleted the claude/issue-2614-rust-receivers branch October 23, 2026 06:25
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 23, 2026
…mchenry#2834; superseded colbymchenry#2260/colbymchenry#2373/colbymchenry#2461/colbymchenry#2625/colbymchenry#2704/colbymchenry#2791 yield to main's versions)

Conflicts: main's versions taken where our PRs were re-implemented upstream
(static/instance override colbymchenry#2813, workspace package entries colbymchenry#2799, daemon lock
colbymchenry#2819, override-overload pairing colbymchenry#2820, Rust typed receivers colbymchenry#2831, explore
long-query perf colbymchenry#2804); both sides kept in resolution/index.ts and
name-matcher.ts (our Kotlin `kotlinCallResultType` renamed to
`kotlinQualifiedCallResultType` beside main's colbymchenry#2822 helper); the fuzzy search
keeps our hard-filter scope on top of main's memoized name list. Tests of the
superseded PRs dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 23, 2026
…per call in a class expression is not the enclosing class's member

The merge took main's colbymchenry#2831 for the conflicting Rust hunks but 167 lines of
the closed colbymchenry#2625 (rustWrittenReceiverType) auto-merged beside them and ruled
out main's own `Sink::flush` case; removed. With main's js-super-calls (colbymchenry#2818),
colbymchenry#1952's enclosing-scope pick read a class expression's `super.run()` as the
enclosing class's and sent it to its parent: a call written on `super` now
skips the pick (the same change goes to PR colbymchenry#1952). js-class-expressions pins
the 0.9 strong match colbymchenry#1952 gives `this.run()` in a class expression.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 23, 2026
…ethod calls in it (#2846)

A Rust method call made through more than one field, or on what another
method returns, was recorded by its bare method name in both extractors:
self.services.store.put("a") became `put`, as did self.store().put("m").
Only a single field kept its receiver (self.store.put). The edge's
refName then said nothing of which value the call goes through, and a
call on a method call's value was never typed, so self.store().put("m")
stayed unresolved. PR #2831 had already made the field-chain calls link
by reading the receiver from the source text, but their names stayed
bare.

Both extractors now keep a receiver that is a chain of fields and method
calls on `self` or on a name, a call written `name()` with its arguments
and turbofish dropped, up to 16 links: self.services.store.put,
self.store().put, vis.map.get, self.0.len, x.get().put. The shapes that
already kept a receiver (x.m, self.m, self.<field>.m, Foo::new().m) are
unchanged; a parenthesized receiver, `?`, `.await`, an index and a longer
chain rooted at a path keep the bare name. The kernel's receiver_chain
mirrors the wasm extractor's rustReceiverChain byte for byte.

Resolution reads such a call as the bare-named call was read before
(rustChainedCallByName, at the top of resolveOneIn), so the frameworks,
the strategies and the scope gates see what they saw, and the edge keeps
the call as written. The chain goes along: the written-receiver hook
reads the receiver the chain names where the call is written
(rustChainedReceiver), so a method call's value is typed from that
method's signature, and a later link that calls the same name again is
told apart instead of being left to a guess by the name.

Validation:
- New __tests__/rust-receiver-chains.test.ts (10 tests): the issue's
  workspace verbatim plus self.store().put("m"), chains whose links call
  the same name through self, a tuple field and a parameter, a call on a
  method's std return value, the recorded shapes and the 16-link cap,
  and a sync that adds the missing method. Red on main's sources, 7 of
  10 failing on both the wasm and the kernel path; green with the fix.
  The Rust kernel parity suite gains a chain case. The extraction test
  that pinned the old collapse now pins the chains.
- A/B against main 865ddf4 (same groups against d833827), both arms
  with the native kernel, every changed call edge read. Per repo: edges
  that only gained their receiver in refName / same target now by type /
  wrong removed / right added / wrong to right:
  ripgrep 3fce3b5b: 159 / 45 / 6 / 6 / 15
  axum 86aa6153: 115 / 25 / 0 / 14 / 7
  tokio 8a2408ed: 1039 / 124 / 36 / 29 / 27, and 1 right link lost
  lemmy 92e077b5: 190 / 24 / 3 / 1 / 3, 1 right lost, 1 wrong added
  The two lost links and the wrong one follow rules the typed-receiver
  reading already applies to typed locals on main: a function-local glob
  `use` does not shadow a file-level named `use` (tokio), a call on a
  dependency's type reaches only impls of project traits, and `x.into()`
  takes the type's explicit `impl Into` (lemmy). Controls gin and
  express: byte-identical dumps. Nodes are identical on every repo.
- Kernel parity (Rust), ripgrep, axum, tokio, lemmy: 1752/1752 files at
  byte parity, 0 deferred.
- Index cost, interleaved median of 3 (wall): ripgrep 3.15 s -> 3.15 s,
  axum 3.39 s -> 3.42 s, tokio 10.48 s -> 10.54 s, lemmy 5.14 s ->
  5.15 s; CPU within noise.

Fixes #2836

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bompus added a commit to bompus/codegraph that referenced this pull request Oct 29, 2026
…ons it exposed (#582)

* fix(resolve): port upstream's lone same-file match confidence (cde4cd7)

A lone same-file definition chosen by findBestMatch scored by directory
proximity (0.7/0.4), losing to a framework's naming guess in another
file. is_decisive_same_file_match scores it 0.9 when it is the only
same-file candidate, is not the caller itself, and, as a member, belongs
to an owner the caller is inside.

Ports upstream cde4cd7 (colbymchenry#1938). same-file-match-confidence: 7/7.

* chore(resolve): dump the kernel profile at close(), not only at drop

The resolver is not dropped before process exit on the CLI index path, so
CODEGRAPH_KERNEL_PROF=1 accumulated timings that never printed. close()
is the deterministic teardown point.

* test(ts): assert the fork's native error-tree path for export type *

Upstream's kernel defers a file whose parse tree has errors, so the test
expected tryKernelExtract to return null. The fork's tsjs walker extracts
the recovered tree natively (cba9256) and keeps the re-export signature
as written — assert that contract instead.

* fix(resolve): port upstream's indexed import-path spelling (8651990)

An alias, workspace or direct-path join kept a specifier's lexical
spelling, so `@/widgets/Chart/` probed `Chart//index.tsx` and
`src/widgets/./Chart` probed `./` segments no files row is keyed by.
probe_extensions now spells each candidate as the index keys it for the
ES-module family; other languages keep the path as found.

Ports upstream 8651990 (colbymchenry#2792). import-path-spelling: 5/5.

* fix(resolve): port upstream's Dart library-privacy rule (f7820b5)

A Dart `_name` is private to its library — the library file and every
file its `part` directives name — not to its file. private_is_file_local
drops `dart`; is_visible_across_files gets the dart arm (a private
candidate is seen only from a file of its own library); and the
exact-name pre-pick filter declines a private candidate another library
declares so the caller's own library's declaration wins.

Ports upstream f7820b5 (colbymchenry#2832). dart-part-privacy: 10/12 — the two
remaining failures resolve a typed receiver's private method through
resolve_method_on_type's member path, whose visibility gate still needs
tracing (upstream's dart arm at name-matcher.ts ~13147 /
preferOwnDartLibrary is the next suspect).

* fix(resolve): vet every strategy's pick through is_visible_across_files

finish() applied name_post_guard but not the file/library-locality veto
resolveOne's nameResult gets at index.ts:1867, so typed-receiver picks
(instance-method) linked private members of another library. With the
dart arm in place this completes f7820b5: dart-part-privacy 12/12.

* fix(library): indexFiles keeps other writers out until its files are stored

The file lock was released as soon as indexing started — the promise
returned unawaited, the try ended, the finally let the lock go. Awaited,
the lock now holds until the files are stored, as indexAll and sync hold
it (upstream 865ddf4 / colbymchenry#2845).

* fix(resolve): let a framework own a reference past the name strategies

settleKernelOutcome merged kernel candidates beside the frameworks', so a
project namesake could answer a call Nuxt's navigateTo owns (upstream
58eb57a / colbymchenry#2826). With ownsReference set, the frameworks' best answer
wins or the ref stays unresolved.

* test(vue): assert the fork contract for tsx blocks — grammar always compiled in

Upstream's test asserted wasm-preload sets (9ee6718 / colbymchenry#2824); the fork
has no wasm loader, so a tsx <script> can never lack its grammar. The
block-mapping half was already fixed in sfcScriptLanguage.

* fix(resolve): finish() visibility guard on the final node, name picks only

Two corrections to the finish() is_visible_across_files port:

- Run it on the node after supertype/overload/alias retargets, the way
  upstream vets the target matchReference reports — a pre-forward object
  property or import alias is not the node upstream inspects.
- Vet only name-matcher picks: upstream's guard sits inside the
  matchReference arm, so import, file-path and framework results never
  reach it. An import pick's alias forward can legitimately land on an
  unexported module member (export const api = { run: right }).

Also ports the Rust arm upstream added for typed receivers: a method a
trait declares carries no pub of its own — reached through a typed
receiver it is the trait's (rust_trait_of + rust_typed_refs, marking
rust_written_call exits). Fixes the rust-* regressions the guard exposed
(rust-typed-receivers, rust-receiver-chains, rust-generic-bound-paths,
rust-method-value-types, rust-self-field-types, rust-wrapped-self-calls)
while keeping dart-part-privacy 12/12.

Remaining exposed failures are import-binding port debt, not guard bugs:
items$.getState().inc (import-type-modifier) and styled(Disclosure)
(function-ref-project-imports) resolve upstream through the import path
the kernel does not fully carry.

* feat(kernel): port Go value-type inference, indexed/chained/package-value call arms

Port goReceiverChainType/goElementOf/goPackageValueType semantics
tree-natively into the kernel: package-level var/const typing, indexed
and chained receiver calls, and declared-type terminal vetoes for
literals/outside types. Bound-receiver misses are terminal unless the
receiver is an asserted local the index cannot type (upstream
strategy-3 passthrough). TS gateTargetKind gains the missing upstream
arms (import self-edge, Go extends qualification, dot-imported
supertypes) and goBareTypeDir for type-position targets.

* fix(kernel): resolve Go package-value calls at typed-receiver confidence

Upstream's matchGoPackageValueCall resolves at 0.9; the arm inherited
go_method_in_package's 0.85.

* feat(kernel): Go element/field/call-result expression typing

goElementOf (named slice/map types resolve through their declarations),
goFieldTypeOf (struct field written types), selector-expression and
call-expression result arms in go_expr_written. Param/decl locals the
declaration reader misses fall back to infer_local_receiver_type.

* feat(kernel): Go chained/generic receivers, range elements, unread-file methods

* feat(kernel): Go build-constraint twins, external test packages, conversion refs (colbymchenry#2581)

Port upstream 800e582's three Go semantics into the kernel:

- A `pkg.Type(…)` conversion resolves its type through the import
  member arm — the kind filter now accepts Go exported member kinds
  (struct/interface/type_alias/variable/constant), which the TS
  edge-kind rewrite then turns into a `references` edge.
- `go_name_visible` is clause-aware: files sharing a directory but
  declaring different package clauses (`a` vs `a_test`) see each
  other's bare top-level names only through an import — qualified,
  or bare after a dot import and exported.
- New `go_build.rs` ports `goFileConstraint`/`preferGoBuildTwin`:
  `//go:build` and `// +build` expressions plus `_GOOS`/`_GOARCH`
  filename suffixes evaluate against the ordered GO_BUILD_TARGETS;
  a resolved target with constrained twins retargets to the twin
  the reference's own file builds with, and `go_pick_twin` collapses
  multi-candidate package members the import arm couldn't pick.

Also fixes the fork's `go-type-visibility.ts`: module-root packages
spell their dir `''` (matching `getGoPackageDir` and kernel
`pos_dirname`), so `goTypePositionTarget` retargets a namesake
function to the module-root type.

* feat(kernel): VB.NET namespace receivers, overload pick, head members (colbymchenry#2762)

Port upstream dbb6aa9: VbReceiver gains a namespace arm
(vb_known_namespaces/vb_namespace_around/vb_namespace_named/
vb_types_in_namespace/vb_module_member_in) so reads like
Billing.Invoice.MaxLines and Acme.Billing.Invoice.MaxLines link the
type or Module member the namespace declares; vb_takes_no_arguments +
vb_runnable_overload pick the overload VB.NET runs when a method is
named without parentheses (Me.Load, Optional/ParamArray, a ctor's
Me.New never resolves to itself); vb_head_member + vb_with_head link a
bare member before the dot (Button1.Text) when no local or parameter
shadows it, gated by foreign-Module and outside-namesake checks.
vb_receiver_type's member search is extracted as vb_member_around.

* feat(kernel): workspace package entries, path deps, manifest-edit sync (colbymchenry#2502, colbymchenry#2456)

Port e8ddc4e's declared-entry import resolution into the kernel and its
sync-side invalidation into codegraph.ts.

Kernel (napi config grows entriesByDir + scopedByName):
- resolve_workspace_import takes from_file; workspace_import_targets tries
  declared entries first — exports (exact, wildcard, fallback arrays,
  nested conditions), then source/module/main/types/typings in order —
  then an indexed direct probe, then emitted→source mapping, then the
  direct probe as last resort. Path deps (file:/link:/portal:) resolve
  scoped by the importer's nearest manifest, before global names;
  longest package name wins.
- imports.rs gains longest_package_name/match_workspace_package/
  declared_workspace_entries/indexed_target/built_source_of plus
  WorkspaceTargets/exported_targets/built_sources_of; prefilter passes
  the importing file through.

TS sync side (upstream index.ts port):
- resolver: refreshWorkspacePackages/getWorkspaceFingerprint/
  workspacePackageDirOf; a moved map closes the kernel since the config
  is construction-time.
- codegraph.ts: js_workspace_fingerprint metadata stamped at index and
  re-checked on full reconcile; a moved fingerprint re-opens every
  ESM-language ref for the orphan sweep. reopenReferencesThroughAddedFiles
  gets packageDirOf so a late-appearing declared entry re-links importers.
- loader.ts KernelWorkspaceIn + serialization gain the two new maps.

workspace-package-entries: 26/26. Regression sweep: 146/147, the one
failure a pre-existing tracked bucket (7180fd0 colbymchenry#2788).

* feat(kernel): a call on a this-field element resolves on the element's type

Port 449eefa's resolver half: `this.items[0].save()` arrives as
`this.items[].save` from the extractors (already ported) and now binds the
element type the field's declaration writes instead of bare-name matching
any project method.

- method_call_shape: elementOf arm `^this\.(#?[\w$]+)\[\]\.([\w$]+)$` →
  match_ts_this_field_call(field, method, r, element=true) — exclusive,
  like the plain `this.field.m` arm.
- TsFieldDecl gains element; ts_field_element_type reads it off the
  written type (`Item[]`, `Array<Item>`, `ReadonlyArray<Item>`,
  `Record<string, Item>`) and rejects unions, arrays of arrays and
  non-plain elements; ts_field_element adds tsFieldRead's multi-line
  continuation guard (`| Other[]` on the lines below).
- ts_field_held filters element reads through ts_class_type_parameter —
  `class Store<Item> { items: Item[] }` holds no project `Item`.
- ts_class_type_parameter ported upstream: `class|interface|type` keyword
  anchor, 24-line multi-line head, returns true when the list's `>` is
  not found (drop the type rather than take a parameter for a class).

ts-this-field-element 11/11; expression-receiver-calls +
ts-array-annotated-receivers green; receiver sweep shows only tracked
debt (08478ee colbymchenry#2811, 013b91c colbymchenry#2825); golden dumps unchanged.

* feat(kernel): calls on a typed local or parameter reach the builtin-named method (colbymchenry#2821)

A python call `reg.get()` on a builtin-named method was dropped by the
builtin filter before any strategy could read the receiver. Port
matchPythonTypedLocalCall (colbymchenry#2583) into python_attribute_verdict: a plain
local or parameter receiver settles only through scope evidence —
`python_local_class` agrees a binding def's parameter annotation (quoted,
Optional[T], Union[..., None], T | None, read off the raw def header) or
its assignments (ctor/factory/None; any foreign binding shape, rebinding
or contested evidence refuses). Members come from `python_strict_members`
— the class's own or a declared base's, resolved through the file's
imports, so a base from outside the project holds none of its methods
and the extends edges never substitute a same-name class.

Sync side: FAILED_PY_ATTR_REF widened to the one-dot `x.y` shape. The
fork's delta-narrowed retry keys (perf/sync-delta-retry) never list an
unchanged name like `get`, so a signature-only edit to the factory a
failed typed-local call reads its type from gave no retry key; the
attr-retry lane kernel-probes candidates and retries only resolvable
ones, which also lands the edge's typeFrom stamp for future edits.

* feat(kernel): a call on an unqualified call result resolves on its return type (colbymchenry#2822)

`repository().pending()` — and `val repo = repository()` locals — type the
receiver by the inner call's declared return, so `pending` is the method the
returned project type declares, never a same-named method elsewhere. A type
from outside the project leaves the name strategies untouched, and an
extension the bound-member walk can't see keeps the old name match.

The extractor's return_type keeps only a user_type's first identifier
(`Request`), so `kotlin_scoped_hit` retypes the callee through its signature
(`(): Request.Builder`) and writes a lone nested name through its declaring
outer (`Builder` → `Shop.Builder`).

Two pre-existing filters were misaligned with the kotlinCall/kotlinBare
split this relies on: member-scope reachability vetted receiver-written
names (`text().reverse`) as if receiver-less, and a Chain-shape call with a
non-project receiver type (`StringBuilder`) vetoed every name match —
upstream only constrains standard-library names there. Both now match
upstream's isReceiverLessKotlinCall / kotlinStdChain boundary.

Also: python_local_class returns Option<Answer> — a name no enclosing def
binds (module-level, global) is upstream's `undefined`, not a refused local,
which restores module-global `store = Store()` receivers the strict arm
claimed and dropped.

* feat(kernel): non-test refs resolve as if Jest manual mocks were absent (colbymchenry#2790)

Port upstream e69546e's mockless context: a ref from non-test code resolves
on a graph without the project's __mocks__/ files, while test code keeps the
whole graph. resolve_ref sets a per-ref mockless flag; nodes_by_name,
nodes_by_lower_name and nodes_by_qualified_name then drop __mocks__/ nodes
(gated on the cached mock-name sets so unaffected names aren't re-collected).
The TS callback-synthesis guards were already in place.

Also fix ts_scoped_reference's type-ref tail: upstream importedTypeMember
falls through (undefined) on a found-miss unless the specifier is outside
the repository — a resolved package directory whose exports can't be read
is not a refusal. The old gate refused references/extends refs bound to a
no-entry workspace package, which upstream answers by name-match.

* fix(sync): record files a JSX tag import reads through as synthesis inputs (colbymchenry#2563)

collectSynthesisInputs only kept files whose own content matched
mayFeedSynthesis, so a barrel or index that merely forwarded another
module's component was never recorded as an input. When a later sync
edited only that index, the JSX pass never reran and the tag kept its
stale endpoint.

Port upstream's importedTagModules: for each JSX-bearing file, map its
capitalized tags to project imports and collect every file
exportedComponent reads through, memoized per collection run.

* feat(kernel): resolve Rust calls by the receiver's written type (colbymchenry#2831)

Port upstream 8fdc8a1: a field chain or struct literal receiver's declared
type decides the call — `vis.map.get` is HashMap's, `Xyz{}.to_val` is Xyz's —
and a path call naming a type std or a dependency owns reaches only the
project's impls of that type or the named trait, never a project namesake.

- rust_expr_type types tuple-struct/enum-variant constructor calls
- undeclared-type guard limited to plain named `field_expression` heads,
  exempting generics, Self, outside types, use-aliases and wrapper-own
  methods; derived-trait provided methods resolve through `#[derive]`
- rust_path_call_reachable gates name-match on the path's outside type

Rust sweep 28 files/206 tests green; kernel golden dumps clean; the 16
remaining full-suite failures are all tracked port debt.

* feat(kernel): scope named class expressions and export them over CommonJS (colbymchenry#2825)

A named class expression is a class bound inside its expression: index
its members and supertypes, but resolve references written outside the
expression on a graph where that class and its members are not — a
same-named declaration or binding keeps its links, and `new Widget()`
elsewhere links nothing. References written inside see the whole graph
unless the name is another expression's, bound inside that one alone.

`module.exports = class Router` / `exports.Store = class StoreImpl`
are the module's value: the class stays reachable to every importer,
and collect_cjs_export records the declared name as the export's
local binding.

Inheritance refs resolved through an import binding emit upstream's
import confidence 0.9; the scoped arm's 0.95 stays for type-position
references.

* fix(kernel): a call or new never lands on an interface or a type alias (colbymchenry#2801)

Nothing calls an interface or a type alias, or makes an instance of one.
A calls/instantiates result that found a TypeScript type by name or
through an import meant the value its file declares beside it — the
function under `interface Ok` in a result module, the class merged with
an interface — and a global the project only types, like `new Response()`
or `new Worker()`, links nothing.

* fix(kernel): an explicit operator call reads its own argument list

tree-sitter-cpp parses `d.operator+=(c)` as a call on the receiver alone,
with the operator's name stranded in an ERROR sibling (`function:
(identifier) (ERROR ".operator+=") arguments: (c)`), so the callee never
spells `operator+=` and cpp_call_argument_types found no call. Every
overload then tied at 0 and the first — `operator+=(int)` — took a call
that `operator+=(const Counter&)` fits. Accept a call whose error spells
the operator name (the member-access dot is part of the error text), so
the argument types are read and the class overload wins.

`size_type` and `difference_type` count as numbers now, the container
convention upstream uses (6bd217a): nlohmann/json's `p->operator[](0)`
reaches the index overload, not the `T*` one.

* fix(kernel): a typed-local receiver keeps the invocation-time binding

The colbymchenry#2821 port (0b564c78) settled a builtin-named method call through
pythonLocalClass alone, which refuses when a name is bound more than
once. `store = A(); store = B(); store.get()` then resolved to nothing,
though the fork's receiver-scope rule has always read the binding in
effect at the call (B).

Read that binding: among plain assignments at or before the call, the
last one decides, and a bare annotation (`store: B`) does not rebind.
python_receiver_uncertain still holds back a same-line replacement and a
later replacement a closure's captured cell sees, and a later binding no
longer poisons an earlier one when the call reads before it. A def's
parameter keeps its annotation when nothing rebinds it.

Three shapes the strict read owns stay with it: a binding that is not a
plain assignment (a branch, loop, `with`, comprehension), a base from
outside the project, and a future binding read from a lambda body. A
receiver naming a class declared here (`File.write(f, "x")`) and a
lambda parameter of the receiver's name keep the ordinary paths.

* fix(resolve): vet only name picks through a module's unexported bindings

2ca95adb applied isVisibleAcrossFiles to every strategy's pick. Upstream
guards just the name match (index.ts:1867), and the port's ESM leg
vetoed picks that were never name guesses: a typed store action
(`items$.getState().inc()`) and an imported component function value
were both dropped as unexported module bindings. Keep every language's
privacy rule for a structured pick, and skip the ESM export check for a
pick a receiver's type or a function value made.

* test(eval): record the json, flask, vite and svelte precision runs

Every case held on each corpus (json 11 absent + 7 present, flask 2
present, vite 6 absent + 2 present, svelte 1 absent).

* fix(kernel): close the review findings on the three fixes

- C++ (P3): a comment inside the access (`b./* note */operator+=(a)`)
  left the comment in the error's text, so the operator name never
  matched. Compare the `operator_name` node the grammar still reads,
  and keep the trimmed-text form only when there is none.
- Python (P2): a local read before its only assignment in the same def
  (`result = store.get(); store = A()`) is an UnboundLocalError, not
  the later value; refuse it, keeping the future-initialization
  exception for a nested def that captures the outer name.
- Python (P2): two conditional bindings in different blocks leave no
  value in effect (`if flag: store = A() else: store = B()`, then
  `store.get()`); refuse. One conditional beside a `None` placeholder
  or a later plain assignment still reads, and a conflicting set whose
  binding in effect may not run stays unknown.
- ESM (P2): a function value is import-justified only when the file's
  own import resolves to the target's file, so a bare name fallback
  can no longer take an unexported namesake from another module. A
  default-exported wrapper still reaches its inner function through
  the default import, and `export default function Name` counts as an
  exported name.

Regression tests: commented operator access, a future local read, two
class-binding branches, and an imported name the module does not export.

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.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.

Rust: method call on std/external or struct-literal receiver links to a same-named project method

1 participant