mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
fix(rust): a call keeps its whole receiver chain, and links through method 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 865ddf4a (same groups against d8338275), 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 commit is contained in:
co-authored by
Claude Opus 5.5
parent
865ddf4ada
commit
9c4794d694
@@ -24,10 +24,13 @@
|
||||
//! - Chained-call re-encode is scoped_identifier-gated (`Foo::new().bar()` →
|
||||
//! `Foo::new().bar`); a call through a field of the enclosing type keeps
|
||||
//! the owner-field shape (`self.inner.run()` → `self.inner.run`, #1585);
|
||||
//! instance chains, parens, `.await`, deeper/non-self field chains, and
|
||||
//! bare `self` receivers all collapse to the bare method name (`self` is
|
||||
//! node kind `self`, not `identifier`, so it dodges SKIP_RECEIVERS by
|
||||
//! falling through). Turbofish callees keep the raw `helper::<T>` text.
|
||||
//! a longer chain of fields and method calls on `self` or a name keeps
|
||||
//! that chain, calls as `name()` (`self.services.store.put()` →
|
||||
//! `self.services.store.put`, `self.store().put()` → `self.store().put`,
|
||||
//! #2836); other instance chains, parens, `?`, `.await`, and longer
|
||||
//! path-rooted chains collapse to the bare method name. A bare `self`
|
||||
//! receiver keeps `self.` (#1861). Turbofish callees drop their type
|
||||
//! arguments (`helper::<T>` → `helper`).
|
||||
//! - `use` emits an import node named by the ROOT module (`crate`/`self`/…),
|
||||
//! one root `imports` ref, then one FULL-path `imports` ref per binding;
|
||||
//! `use x::*` (use_wildcard) emits nothing at all.
|
||||
@@ -927,10 +930,13 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
"call_expression" => {
|
||||
// Chained-call re-encode: ONLY an associated-
|
||||
// Chained-call re-encode: an associated-
|
||||
// function chain (`Foo::new().bar()`, inner
|
||||
// callee a scoped_identifier). Instance chains
|
||||
// keep the bare method name.
|
||||
// callee a scoped_identifier). A chain of
|
||||
// fields and method calls on `self` or a name
|
||||
// (`self.store().put()`, #2836) keeps that
|
||||
// chain; any other instance chain the bare
|
||||
// method name.
|
||||
let inner_fn = r.child_by_field_name("function").map(unwrap_generic_function);
|
||||
let reencode =
|
||||
inner_fn.map(|f| f.kind() == "scoped_identifier").unwrap_or(false);
|
||||
@@ -943,6 +949,8 @@ impl<'t> Walker<'t> {
|
||||
.collect();
|
||||
let inner = strip_turbofish(&inner);
|
||||
callee_name = format!("{inner}().{method_name}");
|
||||
} else if let Some(chain) = receiver_chain(r, self.src) {
|
||||
callee_name = format!("{chain}.{method_name}");
|
||||
} else {
|
||||
callee_name = method_name.to_string();
|
||||
}
|
||||
@@ -952,9 +960,11 @@ impl<'t> Walker<'t> {
|
||||
// field of the enclosing type (#1585): keep the
|
||||
// `self.` prefix so the resolver can type the
|
||||
// field from the owner struct's declaration
|
||||
// (or leave it unresolved). Any other
|
||||
// field_expression receiver — a deeper chain,
|
||||
// a non-self base — keeps the bare name.
|
||||
// (or leave it unresolved). A longer chain of
|
||||
// fields and method calls on `self` or a name
|
||||
// (`self.services.store.put()`, `vis.map.get()`,
|
||||
// #2836) keeps that chain; any other
|
||||
// field_expression receiver the bare name.
|
||||
let base = r.child_by_field_name("value");
|
||||
let field = r.child_by_field_name("field");
|
||||
match (base, field) {
|
||||
@@ -964,7 +974,12 @@ impl<'t> Walker<'t> {
|
||||
let field_name = self.text(f);
|
||||
callee_name = format!("self.{field_name}.{method_name}");
|
||||
}
|
||||
_ => callee_name = method_name.to_string(),
|
||||
_ => {
|
||||
callee_name = match receiver_chain(r, self.src) {
|
||||
Some(chain) => format!("{chain}.{method_name}"),
|
||||
None => method_name.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// `self.method()` — keep the `self.` prefix so the
|
||||
@@ -1628,6 +1643,64 @@ fn unwrap_generic_function(func: Node<'_>) -> Node<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The most links a receiver chain is kept with (receiver_chain).
|
||||
const RECEIVER_CHAIN_MAX_LINKS: usize = 16;
|
||||
|
||||
fn is_rust_ident(s: &str) -> bool {
|
||||
let mut chars = s.chars();
|
||||
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
|
||||
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
/// The receiver chain a method call is written on, when it is fields and
|
||||
/// method calls on `self` or on a name: `self.services.store`,
|
||||
/// `self.store()`, `vis.map`, `self.0`, `x.get::<u8>(1)` — each field by its
|
||||
/// name or index, each call as `name()`, its arguments and turbofish
|
||||
/// dropped. None for any other receiver (a path's call, a literal,
|
||||
/// parentheses, `?`, `.await`, an index, a macro), and past
|
||||
/// RECEIVER_CHAIN_MAX_LINKS links. Mirrors rustReceiverChain in
|
||||
/// TreeSitterExtractor (#2836): both emit the same bytes.
|
||||
fn receiver_chain(node: Node<'_>, src: &str) -> Option<String> {
|
||||
let mut links: Vec<String> = Vec::new();
|
||||
let mut cur = node;
|
||||
while links.len() <= RECEIVER_CHAIN_MAX_LINKS {
|
||||
if cur.kind() == "self" || cur.kind() == "identifier" {
|
||||
let root = &src[cur.byte_range()];
|
||||
if links.is_empty() || !is_rust_ident(root) {
|
||||
return None;
|
||||
}
|
||||
links.reverse();
|
||||
let mut out = root.to_string();
|
||||
for link in &links {
|
||||
out.push('.');
|
||||
out.push_str(link);
|
||||
}
|
||||
return Some(out);
|
||||
}
|
||||
let call = cur.kind() == "call_expression";
|
||||
let field_of = if call {
|
||||
unwrap_generic_function(cur.child_by_field_name("function")?)
|
||||
} else {
|
||||
cur
|
||||
};
|
||||
if field_of.kind() != "field_expression" {
|
||||
return None;
|
||||
}
|
||||
let name = field_of.child_by_field_name("field").map(|f| &src[f.byte_range()]).unwrap_or("");
|
||||
let ok = if call {
|
||||
is_rust_ident(name)
|
||||
} else {
|
||||
is_rust_ident(name) || (!name.is_empty() && name.chars().all(|c| c.is_ascii_digit()))
|
||||
};
|
||||
if !ok {
|
||||
return None;
|
||||
}
|
||||
links.push(if call { format!("{name}()") } else { name.to_string() });
|
||||
cur = field_of.child_by_field_name("value")?;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// A callee path without its turbofish type arguments: `Vec::<u8>::new` →
|
||||
/// `Vec::new`. The brackets nest (`::<Vec<u8>>`), and the `>` of a `->` is
|
||||
/// no closing one (`::<fn() -> u8>`). Mirrors stripRustTurbofish in
|
||||
|
||||
Reference in New Issue
Block a user