Repository navigation
fix(rust): calls on field chains, struct literals and outside-type paths link by type - #2831
Merged
Merged
Conversation
…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>
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>
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.
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:vis.map.get("a"),map: HashMap<String, i32>Ext::get(exact-match 0.9)J::from(i)underuse serde_json::Value as JFail::from(instance-method 0.7)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'smainkeeps 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
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 forident.methodandself.field.methodrefs, so these went to the name strategies:gethad one project method (Ext::get, an associated fn at that), andto_valhad two trait impls, picked by proximity.J::frommatched no qualified name.Jnames no project type, so Strategy 1 found nothing, and a single capital letter slips pastnamesExternalType, which reads it as a possible generic parameter. Strategy 3 then took the crate's onefrom. The same family: under an unrenameduse serde_json::Value,Value::default()linked by qualified name (0.95) to a projectValue'simpl Default.Fix
matchRustWrittenReceiverCall, an exclusive hook besidematchRustAsMutChainCall. For a bare-named Rust call, it reads the call's receiver from the text at the ref's column.self, and a struct literal (alone or in parentheses).rustValueType, and the call is resolved withrustTypedMethodCall, as a typed local's call is.type AtomicUnsignedShort = …insidecfg_has_atomic_u64! { });m.a.get(k).get(j)), since the two refs share a position.#[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).rustFieldValuenow also reads an element of a tuple-typed field (self.pair.0onpair: (Config, u8)), using the existingrustTupleField.isRustPathCallReachable, ingateRustScopebesideisRustForeignImplCallable. A call through a type path whose type the file'suse, or the written path, names from std or a dependency reaches only:impl Blank for serde_json::Value,impl From<Fail> for PathBuf), orW::flush(s)underuse 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'simpl From<Fail> for PathBuf,impl Blank for serde_json::Value, andW::flush(s)underuse std::io::Write as W.name-matcher.tsandindex.tsswapped back in, 9 of the 12 fail (the 3 guards pass on both); with the fix, all 12 pass.resolutionandcross-language-resolutionpass (416 tests).CODEGRAPH_KERNEL_EXPECT=1): 739 of 746 files passed and 6 were skipped; 8,844 tests passed and 82 were skipped. The one failure wasdaemon-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
callsedges were diffed by call site. Each change was triaged by reading the receiver's declaration (or, for a path, the file'suse):.rsfilesHow each repo was checked:
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).What moved:
File::open→MmapChoice::open, and aHashMapfield'sget→ContextModeLimited::get.use std::sync::Mutex/std::sync::atomic::AtomicUsize/std::time::Instanthad been linked to tokio's own loom shims andtime::Instant; std atomic fields went to loom's mutex-backedAtomicU64::store.std::ops::ControlFlowuses had gone to the fake core intest-utils/src/minicore.rs, andlsp_types::Position::newto a projectPosition.Message::Texthad gone to axum's own.args.mode.update(…)→Mode::update, and tokio's split halves'self.0.peer_addr()→TcpStream::peer_addr;with_mutcalls on loomUnsafeCellfields, which had gone toAtomicUsize::with_mut;snap.analysis.hover(…), which had gone toConfig::hover;ParamEnvAndCrate { … }.store()andCommentKind { … }.prefix().d.message.clone()on amessage: Stringfield links to syntax'sast::String::clone. The struct's file hasconst _: () = { 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 misreadStringthe 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.unsync_loadlinks on tokio. A type no node names is now left to the name strategies.UsersShow { … }.with_query_params(…)and addsNamed { id: 1 }.to_uri().Kernel parity (Rust):
scripts/kernel-parity.mjson 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):
Wrapper(x).m()), and a call on a method call's value inside a chain (a.b().m()), still go to the name strategies.useof a project type (use crate::config::Config as Cfg; Cfg::load()) is unchanged (unresolved, as on main).impl HirDisplayWithExpressionStore for TypeRefId,type TypeRefId = Idx<TypeRef>), which isn't found through the alias. Three rust-analyzerhir_fmtcalls there now link nothing; they previously linked to unrelated types.🤖 Generated with Claude Code