Files
codegraph/codegraph-kernel/src/rustlang.rs
T
Colby MchenryandClaude Opus 5.5 9c4794d694 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>
2026-10-23 14:07:45 +00:00

1780 lines
74 KiB
Rust

//! Rust-language extraction — a faithful port of `TreeSitterExtractor`'s rust
//! paths (src/extraction/tree-sitter.ts) plus languages/rust.ts. ("rustlang"
//! because `rust` alone collides with the kernel's own implementation
//! language.) Survey artifact: docs/design/rust-lang-kernel-port-checklist.md.
//!
//! Rust's shape quirks, mirrored exactly (bug-for-bug, all verified against
//! the TS reference):
//! - `isAsync` is dead code upstream: it scans DIRECT children for an `async`
//! token, but the grammar nests it inside `function_modifiers` — every rust
//! fn/method carries isAsync **false** (present-false, never absent).
//! - impl blocks push NO scope: members re-dispatch at file scope, so an impl
//! associated `const` becomes a FILE-level `variable`, and the method↔owner
//! `contains` edge is a source-order name scan (an impl ABOVE its struct
//! gets no edge). The receiver (method QN prefix, `contains` owner,
//! `implements` source) is the impl_item's `type` field via
//! impl_type_name — both sides moved to the grammar's trait:/type: fields
//! together in #1588 (the earlier positional scan qualified every
//! parameterized impl's methods by the TRAIT).
//! - `const_item`/`static_item` ride the generic extractVariable fallback:
//! kind is always `variable`, no signature, and EVERY direct `identifier`
//! child mints a node (`const MAX: u32 = OTHER;` → two nodes, `MAX` + the
//! phantom `OTHER`). Top-level initializer values are never body-walked.
//! - `mod_item` mints no module node and adds no QN prefix.
//! - 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);
//! 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.
//! - Trait supertraits come only from `trait_bounds`; a scoped supertrait
//! (`fmt::Debug`) matches no case and is silently dropped.
//! - Rocket `routes!`/`catchers!` are extracted ONLY inside function bodies,
//! and only when the macro name is a bare identifier.
//! - A rust type alias emits NO ref to its aliased type (the shared code
//! reads a `value` field; rust's field is `type`).
//! - An `attribute_item` between a doc comment and its item breaks the
//! docstring sibling chain (`#[derive(..)]` kills the docstring).
//! Files with parse errors defer to wasm.
use crate::buffers::{
build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow,
RefRow, StrRef, Tables, FLAG_IS_ASYNC, FLAG_IS_EXPORTED, FUNCTION_REF_CODE, NONE, NONE_STR,
};
use crate::docstring::preceding_docstring;
use crate::ids;
use crate::textutil as util;
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
use tree_sitter::{Node, Parser};
const MAX_VALUE_REF_NODES: usize = 20_000;
/// JS `/<[^>]*>/g` — the non-nested generic strip (breaks on nested generics
/// by design: `Result<Vec<Foo>, E>` → `Result, E>` → returnType undefined).
fn generic_angle_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"<[^>]*>").unwrap())
}
/// JS `/^[A-Za-z_]\w*$/` (ASCII \w — the regex crate's \w is Unicode).
fn simple_ident_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"^[A-Za-z_][0-9A-Za-z_]*$").unwrap())
}
/// extractStaticMemberRef's capitalized-receiver test.
fn capitalized_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"^[A-Z][A-Za-z0-9_]*$").unwrap())
}
struct Scope {
row: u32,
kind: &'static str,
name: String,
}
#[derive(Default)]
struct Extra {
docstring: Option<String>,
decorators: Option<String>,
signature: Option<String>,
return_type: Option<String>,
qualified_name: Option<String>,
visibility: Option<u8>,
is_exported: Option<bool>,
is_async: Option<bool>,
}
struct ValueScope<'t> {
row: u32,
node: Node<'t>,
name: String,
}
struct Cand {
from: u32,
name: String,
line: u32,
column_byte: usize,
row: usize,
}
/// Per-node metadata for the receiver-method owner lookup and
/// findNodeByName (mirrors the TS scans over `this.nodes` — FIRST match
/// wins, earlier-in-file only).
struct NodeMeta {
kind: &'static str,
name: String,
}
pub struct Walker<'t> {
src: &'t str,
file_path: &'t str,
line_starts: Vec<usize>,
arena: Arena,
node_id_allocator: ids::NodeIdAllocator,
tables: Tables,
stack: Vec<Scope>,
nodes_meta: Vec<NodeMeta>,
node_ids: Vec<String>,
defined_fn_names: HashSet<String>,
imported_names: HashSet<String>,
fn_ref_cands: Vec<Cand>,
fs_values: HashMap<String, u32>,
fs_value_counts: HashMap<String, u32>,
value_scopes: Vec<ValueScope<'t>>,
}
pub fn extract(file_path: &str, source: &str) -> Result<EmitOut, String> {
let grammar = crate::langs::grammar_for("rust").ok_or("no rust grammar")?;
let t0 = std::time::Instant::now();
let mut parser = Parser::new();
parser
.set_language(&grammar)
.map_err(|e| format!("set_language(rust) failed: {e}"))?;
let tree = parser
.parse(source, None)
.ok_or_else(|| "parser returned null tree".to_string())?;
if tree.root_node().has_error() {
return Err("defer: parse tree contains errors — wasm recovery is canonical".to_string());
}
let mut w = Walker {
src: source,
file_path,
line_starts: util::line_starts(source),
arena: Arena::default(),
node_id_allocator: ids::NodeIdAllocator::default(),
tables: Tables::default(),
stack: Vec::new(),
nodes_meta: Vec::new(),
node_ids: Vec::new(),
defined_fn_names: HashSet::new(),
imported_names: HashSet::new(),
fn_ref_cands: Vec::new(),
fs_values: HashMap::new(),
fs_value_counts: HashMap::new(),
value_scopes: Vec::new(),
};
let line_count = source.bytes().filter(|b| *b == b'\n').count() as u32 + 1;
let base_name = file_path.rsplit(['/', '\\']).next().unwrap_or(file_path);
let mut flags = BoolFlags::default();
flags.set(FLAG_IS_EXPORTED, false);
let file_id = w.arena.put(&ids::file_node_id(file_path));
let name_ref = w.arena.put(base_name);
let qn_ref = w.arena.put(file_path);
w.tables.push_node(&NodeRow {
kind: node_kind_index("file").unwrap(),
visibility: 0,
flags,
start_line: 1,
end_line: line_count,
start_column: 0,
end_column: 0,
name: name_ref,
qualified_name: qn_ref,
id: file_id,
docstring: NONE_STR,
signature: NONE_STR,
decorators: NONE_STR,
type_parameters: NONE_STR,
return_type: NONE_STR,
extra_json: NONE_STR,
});
w.nodes_meta.push(NodeMeta { kind: "file", name: base_name.to_string() });
w.node_ids.push(ids::file_node_id(file_path));
w.stack.push(Scope { row: 0, kind: "file", name: base_name.to_string() });
w.visit_node(tree.root_node());
w.flush_fn_ref_candidates();
w.flush_value_refs(tree.root_node());
w.stack.pop();
let duration_ms = t0.elapsed().as_secs_f64() * 1000.0;
let meta = build_meta(&w.tables, w.arena.len(), NONE_STR, duration_ms);
Ok(EmitOut {
meta,
nodes: w.tables.nodes,
edges: w.tables.edges,
refs: w.tables.refs,
arena: w.arena.into_vec(),
})
}
impl<'t> Walker<'t> {
fn text(&self, node: Node) -> &'t str {
&self.src[node.byte_range()]
}
fn line_of(&self, node: Node) -> u32 {
node.start_position().row as u32 + 1
}
fn col_of(&self, node: Node) -> u32 {
util::col16(self.src, &self.line_starts, node.start_position().row, node.start_byte())
}
fn end_col_of(&self, node: Node) -> u32 {
util::col16(self.src, &self.line_starts, node.end_position().row, node.end_byte())
}
fn top_row(&self) -> u32 {
self.stack.last().map(|s| s.row).unwrap_or(0)
}
/// isInsideClassLikeNode — stack TOP only, file doesn't count.
fn inside_class_like(&self) -> bool {
self.stack
.last()
.map(|s| matches!(s.kind, "class" | "struct" | "union" | "interface" | "trait" | "enum" | "module"))
.unwrap_or(false)
}
fn push_ref_at(&mut self, from_row: u32, name: &str, kind_code: u8, node: Node) {
let name_ref = self.arena.put(name);
self.tables.push_ref(&RefRow {
from_idx: from_row,
kind: kind_code,
line: self.line_of(node),
column: self.col_of(node),
reference_name: name_ref,
candidates: NONE_STR,
from_id_str: NONE_STR,
});
if kind_code == edge_kind_index("imports").unwrap() {
if util::simple_name().is_match(name) {
self.imported_names.insert(name.to_string());
} else if let Some(c) = util::qualified_import().captures(name) {
// `::`-separated rust paths match NEITHER regex (separators are
// `.`/`\`), so multi-segment use-imports contribute nothing to
// the fn-ref gate — the rust gate is effectively same-file-only.
self.imported_names.insert(c[1].to_string());
}
}
}
fn create_node(&mut self, kind: &'static str, name: &str, node: Node<'t>, extra: Extra) -> Option<u32> {
if name.is_empty() {
return None;
}
let start_line = self.line_of(node);
let column = self.col_of(node);
let id = self.node_id_allocator.generate(self.file_path, kind, name, start_line, column);
let end_line = node.end_position().row as u32 + 1;
let qualified = extra.qualified_name.unwrap_or_else(|| {
let mut parts: Vec<&str> = Vec::new();
for s in &self.stack {
if s.kind != "file" {
parts.push(&s.name);
}
}
let mut qn = parts.join("::");
if !qn.is_empty() {
qn.push_str("::");
}
qn.push_str(name);
qn
});
let mut flags = BoolFlags::default();
if let Some(v) = extra.is_exported {
flags.set(FLAG_IS_EXPORTED, v);
}
if let Some(v) = extra.is_async {
flags.set(FLAG_IS_ASYNC, v);
}
let name_ref = self.arena.put(name);
let qn_ref = self.arena.put(&qualified);
let id_ref = self.arena.put(&id);
let doc_ref = opt_str(&mut self.arena, extra.docstring.as_deref());
let sig_ref = opt_str(&mut self.arena, extra.signature.as_deref());
let decorators_ref = opt_str(&mut self.arena, extra.decorators.as_deref());
let ret_ref = opt_str(&mut self.arena, extra.return_type.as_deref());
let row = self.tables.push_node(&NodeRow {
kind: node_kind_index(kind).unwrap(),
visibility: extra.visibility.unwrap_or(0),
flags,
start_line,
end_line,
start_column: self.col_of(node),
end_column: self.end_col_of(node),
name: name_ref,
qualified_name: qn_ref,
id: id_ref,
docstring: doc_ref,
signature: sig_ref,
decorators: decorators_ref,
type_parameters: NONE_STR,
return_type: ret_ref,
extra_json: NONE_STR,
});
self.nodes_meta.push(NodeMeta { kind, name: name.to_string() });
self.node_ids.push(id);
let parent_row = self.top_row();
self.tables.push_edge(&EdgeRow {
source_idx: parent_row,
target_idx: row,
kind: edge_kind_index("contains").unwrap(),
provenance: 0,
line: NONE,
column: NONE,
metadata_json: NONE_STR,
source_id_str: NONE_STR,
target_id_str: NONE_STR,
});
if kind == "function" || kind == "method" {
self.defined_fn_names.insert(name.to_string());
}
// captureValueRefScope: rust consts are kind `variable` — still targets.
let target_kind_ok = kind == "constant" || kind == "variable";
if target_kind_ok
&& util::utf16_len(name) >= 3
&& util::has_upper_or_underscore().is_match(name)
{
let parent_ok = self
.stack
.last()
.map(|s| matches!(s.kind, "file" | "class" | "module" | "struct" | "union" | "enum"))
.unwrap_or(false);
if parent_ok {
self.fs_values.insert(name.to_string(), row);
*self.fs_value_counts.entry(name.to_string()).or_insert(0) += 1;
}
}
if matches!(kind, "function" | "method" | "constant" | "variable") {
self.value_scopes.push(ValueScope { row, node, name: name.to_string() });
}
Some(row)
}
/// extractName — nameField `name`, else the identifier-like child scan.
fn extract_name(&self, node: Node) -> String {
if let Some(name_node) = node.child_by_field_name("name") {
return self.text(name_node).to_string();
}
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
if matches!(c.kind(), "identifier" | "type_identifier" | "simple_identifier" | "constant") {
return self.text(c).to_string();
}
}
}
"<anonymous>".to_string()
}
/// rustExtractor.getSignature: raw params text + ` -> ` + raw return type.
fn signature_of(&self, node: Node) -> Option<String> {
let params = node.child_by_field_name("parameters")?;
let mut sig = self.text(params).to_string();
if let Some(rt) = node.child_by_field_name("return_type") {
sig.push_str(" -> ");
sig.push_str(self.text(rt));
}
Some(sig)
}
/// rustExtractor.getVisibility: direct `visibility_modifier` child whose
/// text contains `pub` → public, else private; none → private.
fn visibility_of(&self, node: Node) -> u8 {
for i in 0..node.child_count() {
if let Some(c) = node.child(i) {
if c.kind() == "visibility_modifier" {
return if self.text(c).contains("pub") { 1 } else { 2 };
}
}
}
2 // private — Rust defaults to private
}
/// extractRustReturnType (languages/rust.ts:14).
fn return_type_of(&self, node: Node) -> Option<String> {
let mut rt = node.child_by_field_name("return_type")?;
if rt.kind() == "reference_type" {
rt = (0..rt.named_child_count())
.filter_map(|i| rt.named_child(i))
.find(|c| matches!(c.kind(), "type_identifier" | "scoped_type_identifier" | "generic_type"))
.unwrap_or(rt);
}
if matches!(rt.kind(), "primitive_type" | "unit_type" | "tuple_type") {
return None;
}
let text = self.text(rt).trim();
let stripped = generic_angle_re().replace_all(text, "");
let last = stripped.rsplit("::").next().unwrap_or("").trim();
if last.is_empty() || !simple_ident_re().is_match(last) {
return None;
}
Some(if last == "Self" { "self".to_string() } else { last.to_string() })
}
/// rustImplTypeName (languages/rust.ts) — the implementing type's simple
/// name for an impl block, from the grammar's `type` field (#1588):
/// `impl<T> Tr for G<T>` / `impl<'a> Iterator for Parents<'a>` /
/// `impl Tr for &Foo` / `impl Tr for m::Foo` → `G` / `Parents` / `Foo` /
/// `Foo`. Shapes naming no single type (tuple, `dyn Tr`, pointer,
/// primitive, fn type…) → None. Mirrored byte-for-byte — change both.
fn impl_type_name(&self, ty: Option<Node>) -> Option<String> {
let ty = ty?;
match ty.kind() {
"type_identifier" | "identifier" => Some(self.text(ty).to_string()),
"generic_type" => self.impl_type_name(ty.child_by_field_name("type")),
"scoped_type_identifier" | "scoped_identifier" => {
self.impl_type_name(ty.child_by_field_name("name"))
}
"reference_type" => self.impl_type_name(ty.child_by_field_name("type")),
_ => None,
}
}
/// rustExtractor.getReceiverType: parent-walk to the nearest impl_item and
/// read its `type` field (impl_type_name). The pre-#1588 rule took the
/// LAST direct type_identifier child, which for `impl Trait for Generic<T>`
/// was the TRAIT — so every parameterized impl's methods were qualified by
/// the trait.
fn receiver_type_of(&self, node: Node) -> Option<String> {
let mut parent = node.parent();
while let Some(p) = parent {
if p.kind() == "impl_item" {
return self.impl_type_name(p.child_by_field_name("type"));
}
parent = p.parent();
}
None
}
/// rustImplItemOwner (languages/rust.ts) — the type an item written
/// directly in an impl block belongs to: `ValueHint` for the `type Err`
/// of `impl FromStr for ValueHint`, then `ValueHint::Err`, as the impl's
/// methods are. A type no single name stands for (`()`, `(A, B)`, `[u8]`,
/// `u32`) is written as the impl writes it. Mirrored byte-for-byte —
/// change both.
fn impl_item_owner(&self, node: Node) -> Option<String> {
let list = node.parent().filter(|p| p.kind() == "declaration_list")?;
let imp = list.parent().filter(|p| p.kind() == "impl_item")?;
let mut ty = imp.child_by_field_name("type")?;
while ty.kind() == "reference_type" {
ty = ty.child_by_field_name("type")?;
}
self.impl_type_name(Some(ty)).or_else(|| {
let words: Vec<&str> =
self.text(ty).split([' ', '\t', '\r', '\n']).filter(|w| !w.is_empty()).collect();
Some(words.join(" "))
})
}
// --- visitNode ------------------------------------------------------------
fn visit_node(&mut self, node: Node<'t>) {
stack_guard!();
let kind = node.kind();
let mut skip_children = false;
self.maybe_capture_fn_refs(node);
if matches!(kind, "function_item" | "function_signature_item") {
self.extract_fn_or_method(node);
skip_children = true;
} else if kind == "trait_item" {
self.extract_interface(node);
skip_children = true;
} else if kind == "struct_item" {
self.extract_aggregate(node, "struct");
skip_children = true;
} else if kind == "union_item" {
self.extract_aggregate(node, "union");
skip_children = true;
} else if kind == "enum_item" {
self.extract_enum(node);
skip_children = true;
} else if kind == "type_item" {
self.extract_type_alias(node);
// extractTypeAlias returns false for rust (plain alias) — children
// are visited (nothing in them has a branch).
} else if matches!(kind, "let_declaration" | "const_item" | "static_item")
&& !self.inside_class_like()
{
// Inside a class-like scope the gate fails and the else-ladder
// falls through with children VISITED — a trait const's value
// expression emits calls refs from the trait node.
// The initializer extract_variable walked under the declared
// symbol took its own candidates; the scan covers the rest.
let walked = self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0, &walked);
skip_children = true;
} else if kind == "use_declaration" {
self.extract_import(node);
// importTypes branch never sets skipChildren.
} else if kind == "call_expression" {
self.extract_call(node);
} else if kind == "struct_expression" {
self.extract_instantiation(node);
} else if kind == "impl_item" {
// Emits the implements back-reference; skipChildren stays false so
// the declaration_list is visited at FILE scope (impl pushes
// nothing on the stack).
self.extract_rust_impl_item(node);
}
if !skip_children {
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.visit_node(c);
}
}
}
}
// --- extractors --------------------------------------------------------------
/// extractFunction/extractMethod, decision resolved once: method iff a
/// receiver is found (fn inside an impl — including a NESTED fn inside an
/// impl method's body, whose parent walk passes through the outer fn) or
/// the stack top is class-like (trait members).
fn extract_fn_or_method(&mut self, node: Node<'t>) {
stack_guard!();
let receiver = self.receiver_type_of(node);
let as_method = receiver.is_some() || self.inside_class_like();
let name = self.extract_name(node);
if name == "<anonymous>" {
if let Some(body) = node.child_by_field_name("body") {
self.visit_function_body(body);
}
return;
}
// Match the wasm walker's sibling-attribute handling for Tauri commands.
let mut decorators = None;
if node.kind() == "function_item" {
let mut sibling = node.prev_named_sibling();
while let Some(item) = sibling {
match item.kind() {
"line_comment" | "block_comment" => {},
"attribute_item" => {
let name = item.named_child(0).and_then(|a| a.named_child(0));
if let Some(name) = name {
let text: String = self.src[name.byte_range()].chars()
.filter(|c| !c.is_whitespace()).collect();
if text == "tauri::command" {
decorators = Some("tauri::command".to_string());
break;
}
}
},
_ => break,
}
sibling = item.prev_named_sibling();
}
}
let extra = Extra {
decorators,
docstring: preceding_docstring(node, self.src),
signature: self.signature_of(node),
visibility: Some(self.visibility_of(node)),
// isAsync hook exists but never finds a direct `async` child (it
// nests in function_modifiers) — present-false on every node.
is_async: Some(false),
return_type: self.return_type_of(node),
qualified_name: receiver.as_ref().map(|r| format!("{r}::{name}")),
..Extra::default() // isExported hook absent → flag not set
};
let kind: &'static str = if as_method { "method" } else { "function" };
let Some(row) = self.create_node(kind, &name, node, extra) else { return };
// Contains edge from the owner: receiver present AND not class-like —
// FIRST earlier-in-file struct/class/enum/trait of the receiver's name.
if as_method && !self.inside_class_like() {
if let Some(receiver) = &receiver {
let owner_row = self
.nodes_meta
.iter()
.position(|m| {
m.name == *receiver
&& matches!(m.kind, "struct" | "union" | "class" | "enum" | "trait")
})
.map(|i| i as u32);
if let Some(owner_row) = owner_row {
self.tables.push_edge(&EdgeRow {
source_idx: owner_row,
target_idx: row,
kind: edge_kind_index("contains").unwrap(),
provenance: 0,
line: NONE,
column: NONE,
metadata_json: NONE_STR,
source_id_str: NONE_STR,
target_id_str: NONE_STR,
});
}
}
}
self.extract_type_annotations(node, row);
self.stack.push(Scope { row, kind, name });
if let Some(body) = node.child_by_field_name("body") {
self.visit_function_body(body);
}
self.stack.pop();
}
/// extractInterface — kind `trait` (interfaceKind), inheritance from
/// trait_bounds, body children visited with the trait pushed.
fn extract_interface(&mut self, node: Node<'t>) {
stack_guard!();
let name = self.extract_name(node);
let extra = Extra {
docstring: preceding_docstring(node, self.src),
..Extra::default() // no visibility/isExported on the interface path
};
let Some(row) = self.create_node("trait", &name, node, extra) else { return };
self.extract_inheritance(node, row);
self.stack.push(Scope { row, kind: "trait", name });
let body = node.child_by_field_name("body").unwrap_or(node);
for i in 0..body.named_child_count() {
if let Some(c) = body.named_child(i) {
self.visit_node(c);
}
}
self.stack.pop();
}
/// Extract a Rust struct or union — the body field is OPTIONAL. A unit
/// struct (`struct U;`) has no body and is still a complete definition,
/// so it mints a node with no members; tuple structs' ordered_field_declaration_list
/// is a body. Mirrors the TS reference's `allowBodilessStruct`.
fn extract_aggregate(&mut self, node: Node<'t>, kind: &'static str) {
stack_guard!();
let name = self.extract_name(node);
let extra = Extra {
docstring: preceding_docstring(node, self.src),
visibility: Some(self.visibility_of(node)),
..Extra::default()
};
let Some(row) = self.create_node(kind, &name, node, extra) else { return };
self.extract_inheritance(node, row);
// Unit structs have no body to walk — the node itself is the whole
// definition.
let Some(body) = node.child_by_field_name("body") else { return };
self.stack.push(Scope { row, kind, name });
for i in 0..body.named_child_count() {
if let Some(c) = body.named_child(i) {
self.visit_node(c);
}
}
self.stack.pop();
}
/// extractEnum — body required; enum_variant children → enum_member nodes
/// (name field only, payloads never walked); other children re-dispatched.
fn extract_enum(&mut self, node: Node<'t>) {
stack_guard!();
let Some(body) = node.child_by_field_name("body") else { return };
let name = self.extract_name(node);
let extra = Extra {
docstring: preceding_docstring(node, self.src),
visibility: Some(self.visibility_of(node)),
..Extra::default()
};
let Some(row) = self.create_node("enum", &name, node, extra) else { return };
self.extract_inheritance(node, row);
self.stack.push(Scope { row, kind: "enum", name });
for i in 0..body.named_child_count() {
let Some(c) = body.named_child(i) else { continue };
if c.kind() == "enum_variant" {
if let Some(name_node) = c.child_by_field_name("name") {
let vname = self.text(name_node).to_string();
self.create_node("enum_member", &vname, c, Extra::default());
}
} else {
self.visit_node(c);
}
}
self.stack.pop();
}
/// extractTypeAlias — plain `type_alias` node. QUIRK: the alias-value ref
/// walk reads a `value` field; rust type_item's field is `type` → no ref
/// to the aliased type. Returns children-visited (false) like the TS.
fn extract_type_alias(&mut self, node: Node<'t>) {
let name = self.extract_name(node);
if name == "<anonymous>" {
return;
}
let extra = Extra {
docstring: preceding_docstring(node, self.src),
// Private unless `pub` (any form), as the wasm walker reads it:
// the resolver keeps a private alias to its own module.
visibility: Some(self.visibility_of(node)),
// An impl's associated type is its type's (`ValueHint::Err`),
// named only through a path, never bare.
qualified_name: self.impl_item_owner(node).map(|o| format!("{o}::{name}")),
..Extra::default()
};
self.create_node("type_alias", &name, node, extra);
}
/// extractVariable's generic fallback: kind is ALWAYS `variable` (no
/// isConst hook), every direct `identifier` child mints a node positioned
/// at the CHILD, docstring shared, isExported present-false, no signature.
/// The initializer is walked under the declared symbol (#693), and returned
/// then for the declaration scan to skip.
fn extract_variable(&mut self, node: Node<'t>) -> Vec<usize> {
let docstring = preceding_docstring(node, self.src);
let name_field = node.child_by_field_name("name");
// An impl's associated const is its type's (`Limits::MAX`), as its methods are.
let owner = self.impl_item_owner(node);
let mut declared: Option<(u32, String)> = None;
for i in 0..node.named_child_count() {
let Some(child) = node.named_child(i) else { continue };
if child.kind() != "identifier" {
continue;
}
let name = self.text(child).to_string();
if !name.is_empty() {
let row = self.create_node(
"variable",
&name,
child,
Extra {
docstring: docstring.clone(),
is_exported: Some(false),
qualified_name: owner.as_ref().map(|o| format!("{o}::{name}")),
..Extra::default()
},
);
if let (Some(row), Some(nf)) = (row, name_field) {
if child.start_byte() == nf.start_byte() {
declared = Some((row, name));
}
}
}
}
// Walk the initializer ATTRIBUTED to the declared symbol (#693):
// `const N: usize = compute()` and
// `static REGISTRY: Lazy<T> = Lazy::new(|| build())` dropped every call
// inside the initializer, so a handler table or a lazily-built
// singleton linked to nothing.
let mut walked = Vec::new();
if let Some(value) = node.child_by_field_name("value") {
match declared {
Some((row, name)) => {
self.stack.push(Scope { row, kind: "variable", name });
self.visit_function_body(value);
self.stack.pop();
walked.push(value.id());
}
None => self.visit_function_body(value),
}
}
walked
}
/// extractImport via the rust hook: import node named by the ROOT module +
/// one generic root `imports` ref + per-binding FULL-path refs.
/// `use x::*;` (use_wildcard) → hook returns null → nothing at all.
fn extract_import(&mut self, node: Node<'t>) {
let use_arg = (0..node.named_child_count())
.filter_map(|i| node.named_child(i))
.find(|c| matches!(c.kind(), "scoped_use_list" | "scoped_identifier" | "use_list" | "identifier"));
let Some(use_arg) = use_arg else { return };
let module_name = self.root_module(use_arg);
let signature = self.text(node).trim().to_string();
self.create_node(
"import",
&module_name.clone(),
node,
Extra { signature: Some(signature), ..Extra::default() },
);
let parent = self.top_row();
let imports_kind = edge_kind_index("imports").unwrap();
if !module_name.is_empty() {
self.push_ref_at(parent, &module_name, imports_kind, node);
}
self.emit_use_binding_refs(node, parent);
}
/// getRootModule (languages/rust.ts:124).
fn root_module(&self, n: Node) -> String {
stack_guard!();
let Some(first) = n.named_child(0) else {
return self.text(n).to_string();
};
match first.kind() {
"identifier" | "crate" | "super" | "self" => self.text(first).to_string(),
"scoped_identifier" => self.root_module(first),
_ => self.text(first).to_string(),
}
}
/// emitRustUseBindingRefs (tree-sitter.ts:3451) — one FULL-path `imports`
/// ref per binding; `Path as Alias` links the source path; leaves that are
/// only `self`/`super`/`crate`/`*` are skipped.
fn emit_use_binding_refs(&mut self, node: Node<'t>, from_row: u32) {
let mut paths: Vec<(String, Node)> = Vec::new();
fn join(prefix: &str, seg: &str) -> String {
if prefix.is_empty() { seg.to_string() } else { format!("{prefix}::{seg}") }
}
fn collect<'t>(w: &Walker<'t>, n: Node<'t>, prefix: &str, paths: &mut Vec<(String, Node<'t>)>) {
stack_guard!();
match n.kind() {
"identifier" => paths.push((join(prefix, w.text(n)), n)),
"scoped_identifier" => {
let full = w.text(n).trim();
paths.push((
if prefix.is_empty() { full.to_string() } else { format!("{prefix}::{full}") },
n,
));
}
"scoped_use_list" => {
let seg = n
.child_by_field_name("path")
.map(|p| w.text(p).trim().to_string())
.unwrap_or_default();
let new_prefix = if seg.is_empty() { prefix.to_string() } else { join(prefix, &seg) };
let list = n.child_by_field_name("list").or_else(|| {
(0..n.named_child_count())
.filter_map(|i| n.named_child(i))
.find(|c| c.kind() == "use_list")
});
if let Some(list) = list {
collect(w, list, &new_prefix, paths);
}
}
"use_list" => {
for i in 0..n.named_child_count() {
if let Some(c) = n.named_child(i) {
collect(w, c, prefix, paths);
}
}
}
"use_as_clause" => {
let p = n.child_by_field_name("path").or_else(|| n.named_child(0));
if let Some(p) = p {
collect(w, p, prefix, paths);
}
}
_ => {} // visibility_modifier, use_wildcard, bare crate/self/super
}
}
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
collect(self, c, "", &mut paths);
}
}
let imports_kind = edge_kind_index("imports").unwrap();
for (text, n) in paths {
let leaf = text.rsplit("::").next().unwrap_or("");
if leaf.is_empty() || matches!(leaf, "self" | "super" | "crate" | "*") {
continue;
}
self.push_ref_at(from_row, &text, imports_kind, n);
}
}
/// extractCall — the rust paths of the generic else-branch (4312+).
fn extract_call(&mut self, node: Node<'t>) {
if self.stack.is_empty() {
return;
}
let func = node
.child_by_field_name("function")
.or_else(|| node.named_child(0));
// A turbofish — `s.get::<u32>()`, `helper::<u8>()`,
// `Store::with_capacity::<u8>(1)` — calls the function it wraps: its
// type arguments are no part of the callee's name. Mirrors
// TreeSitterExtractor.extractCall.
let func = func.map(unwrap_generic_function);
let mut callee_name = String::new();
if let Some(func) = func {
if func.kind() == "field_expression" {
let property = func
.child_by_field_name("property")
.or_else(|| func.child_by_field_name("field"))
.or_else(|| func.named_child(1));
if let Some(property) = property {
let method_name = self.text(property);
let receiver = func
.child_by_field_name("object")
.or_else(|| func.child_by_field_name("operand"))
.or_else(|| func.child_by_field_name("argument"))
.or_else(|| func.named_child(0));
if let Some(r) = receiver {
if is_literal_receiver(r.kind()) {
return; // emit NOTHING (#1230)
}
}
if let Some(r) = receiver {
match r.kind() {
// rust `self` is node kind `self`, NOT `identifier` —
// it dodges this branch and falls to the bare-name
// fallthrough (same net effect as SKIP_RECEIVERS).
"identifier" | "simple_identifier" | "field_identifier" => {
let receiver_name = self.text(r);
if !matches!(receiver_name, "self" | "this" | "cls" | "super") {
callee_name = format!("{receiver_name}.{method_name}");
} else {
callee_name = method_name.to_string();
}
}
"call_expression" => {
// Chained-call re-encode: an associated-
// function chain (`Foo::new().bar()`, inner
// 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);
if reencode {
let inner: String = self
.text(inner_fn.unwrap())
.replace("->", ".")
.chars()
.filter(|c| !c.is_whitespace())
.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();
}
}
"field_expression" => {
// `self.<field>.<method>()` — a call through a
// 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). 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) {
(Some(b), Some(f))
if b.kind() == "self" && f.kind() == "field_identifier" =>
{
let field_name = self.text(f);
callee_name = format!("self.{field_name}.{method_name}");
}
_ => {
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
// resolver can read the owner off the calling
// method's qualified name and resolve the method on
// THAT type, instead of matching a bare name by file
// proximity (#1861). Mirrors the wasm extractor.
"self" => {
callee_name = format!("self.{method_name}");
}
_ => {
// parenthesized, await_expression — bare method
// name.
callee_name = method_name.to_string();
}
}
} else {
callee_name = method_name.to_string();
}
}
} else if matches!(func.kind(), "scoped_identifier" | "scoped_call_expression") {
// A path's turbofish is no part of the name either
// (`Vec::<u8>::with_capacity` → `Vec::with_capacity`).
callee_name = strip_turbofish(self.text(func));
} else {
// identifier (a turbofish was unwrapped above).
callee_name = self.text(func).to_string();
}
}
if !callee_name.is_empty() {
// Parenthesized-callee normalization — `(f)(x)` → `f`.
if let Some(c) = util::paren_conversion().captures(&callee_name) {
callee_name = c[1].to_string();
}
let from = self.top_row();
self.push_ref_at(from, &callee_name.clone(), edge_kind_index("calls").unwrap(), node);
}
}
/// extractInstantiation — struct_expression via the GENERIC path: strip
/// from the first `<`, keep the trailing `::`/`.` segment (JS slice
/// semantics: slice(lastDot+1) after a `::` leaves one `:`, then ONE
/// leading `[:.]` is stripped).
fn extract_instantiation(&mut self, node: Node<'t>) {
if self.stack.is_empty() {
return;
}
let ctor = node
.child_by_field_name("constructor")
.or_else(|| node.child_by_field_name("type"))
.or_else(|| node.child_by_field_name("name"))
.or_else(|| node.named_child(0));
let Some(ctor) = ctor else { return };
let mut class_name = self.text(ctor).to_string();
if let Some(lt) = class_name.find('<') {
if lt > 0 {
class_name.truncate(lt);
}
}
let last_dot = class_name.rfind('.').map(|i| i as i64).unwrap_or(-1);
let last_colon = class_name.rfind("::").map(|i| i as i64).unwrap_or(-1);
let last = last_dot.max(last_colon);
if last >= 0 {
class_name = class_name[(last + 1) as usize..].to_string();
if let Some(rest) = class_name.strip_prefix(&[':', '.'][..]) {
class_name = rest.to_string();
}
}
let class_name = class_name.trim().to_string();
if !class_name.is_empty() {
let from = self.top_row();
self.push_ref_at(from, &class_name, edge_kind_index("instantiates").unwrap(), node);
}
}
/// extractRustRouteMacro — body-walker-only; bare `routes`/`catchers`
/// identifiers only (`rocket::routes![…]` is skipped); identifier runs in
/// the token tree join with `::`, flushed on `,` and at end.
fn extract_rust_route_macro(&mut self, node: Node<'t>) {
let Some(macro_name) = node.named_child(0) else { return };
let name = self.text(macro_name);
if name != "routes" && name != "catchers" {
return;
}
let token_tree = (0..node.named_child_count())
.filter_map(|i| node.named_child(i))
.find(|c| c.kind() == "token_tree");
let Some(token_tree) = token_tree else { return };
if self.stack.is_empty() {
return;
}
let from = self.top_row();
let refs_kind = edge_kind_index("references").unwrap();
let mut parts: Vec<&str> = Vec::new();
let mut line = 0u32;
let mut column_byte = 0usize;
let mut row = 0usize;
macro_rules! flush {
() => {
if !parts.is_empty() {
let joined = parts.join("::");
let column = util::col16(self.src, &self.line_starts, row, column_byte);
let name_ref = self.arena.put(&joined);
self.tables.push_ref(&RefRow {
from_idx: from,
kind: refs_kind,
line,
column,
reference_name: name_ref,
candidates: NONE_STR,
from_id_str: NONE_STR,
});
parts.clear();
}
};
}
for i in 0..token_tree.child_count() {
let Some(t) = token_tree.child(i) else { continue };
if t.kind() == "identifier" {
if parts.is_empty() {
line = t.start_position().row as u32 + 1;
column_byte = t.start_byte();
row = t.start_position().row;
}
parts.push(self.text(t));
} else if t.kind() == "," {
flush!();
}
}
flush!();
}
/// extractInheritance — the rust-reachable cases: trait_bounds
/// (supertraits; a scoped `fmt::Debug` bound matches NO case and is
/// dropped) and the field_declaration_list recursion. The TS Go-embedding
/// check on field_declaration is gated to Go (and was inert here anyway —
/// every rust field has a field_identifier).
fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
stack_guard!();
let extends_kind = edge_kind_index("extends").unwrap();
for i in 0..node.named_child_count() {
let Some(child) = node.named_child(i) else { continue };
match child.kind() {
"trait_bounds" => {
for j in 0..child.named_child_count() {
let Some(bound) = child.named_child(j) else { continue };
let type_node: Option<Node> = match bound.kind() {
"type_identifier" => Some(bound),
"generic_type" => (0..bound.named_child_count())
.filter_map(|k| bound.named_child(k))
.find(|c| c.kind() == "type_identifier"),
"higher_ranked_trait_bound" => {
let generic = (0..bound.named_child_count())
.filter_map(|k| bound.named_child(k))
.find(|c| c.kind() == "generic_type");
generic
.and_then(|g| {
(0..g.named_child_count())
.filter_map(|k| g.named_child(k))
.find(|c| c.kind() == "type_identifier")
})
.or_else(|| {
(0..bound.named_child_count())
.filter_map(|k| bound.named_child(k))
.find(|c| c.kind() == "type_identifier")
})
}
_ => None, // scoped_type_identifier: dropped (quirk)
};
if let Some(tn) = type_node {
let name = self.text(tn).to_string();
self.push_ref_at(class_row, &name, extends_kind, tn);
}
}
}
"field_declaration_list" | "class_heritage" => {
self.extract_inheritance(child, class_row);
}
_ => {}
}
}
}
/// extractRustImplItem — `impl Trait for Type` back-reference from the
/// grammar's `trait` / `type` fields (#1588; an inherent impl has no
/// `trait` field and emits nothing). Target = FIRST earlier node of kind
/// struct/union/enum/class (never trait) named by impl_type_name; ref FROM
/// the type's node, named by the trait's full text (scoped path / generic
/// args kept), at the trait node's position.
fn extract_rust_impl_item(&mut self, node: Node<'t>) {
let Some(trait_node) = node.child_by_field_name("trait") else {
return;
};
let trait_name = self.text(trait_node).to_string();
let Some(type_name) = self.impl_type_name(node.child_by_field_name("type")) else {
return;
};
let target_row = self
.nodes_meta
.iter()
.position(|m| m.name == type_name && matches!(m.kind, "struct" | "union" | "enum" | "class"))
.map(|i| i as u32);
if let Some(target_row) = target_row {
self.push_ref_at(target_row, &trait_name, edge_kind_index("implements").unwrap(), trait_node);
}
}
/// extractTypeAnnotations — parameters + return_type subtrees, one
/// `references` ref per type_identifier leaf not in BUILTIN_TYPES. The
/// trailing `type_annotation` child lookup is included for fidelity (the
/// rust grammar has no such node — always a no-op).
fn extract_type_annotations(&mut self, node: Node<'t>, from_row: u32) {
if let Some(params) = node.child_by_field_name("parameters") {
self.extract_type_refs_from_subtree(params, from_row);
}
if let Some(ret) = node.child_by_field_name("return_type") {
self.extract_type_refs_from_subtree(ret, from_row);
}
let type_annotation = (0..node.named_child_count())
.filter_map(|i| node.named_child(i))
.find(|c| c.kind() == "type_annotation");
if let Some(ta) = type_annotation {
self.extract_type_refs_from_subtree(ta, from_row);
}
}
fn extract_type_refs_from_subtree(&mut self, node: Node<'t>, from_row: u32) {
stack_guard!();
if node.kind() == "type_identifier" {
let type_name = self.text(node).to_string();
if !type_name.is_empty() && !is_builtin_type(&type_name) {
self.push_ref_at(from_row, &type_name, edge_kind_index("references").unwrap(), node);
}
return;
}
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.extract_type_refs_from_subtree(c, from_row);
}
}
}
// --- visitFunctionBody -----------------------------------------------------
fn visit_function_body(&mut self, body: Node<'t>) {
stack_guard!();
self.visit_for_calls_and_structure(body, None);
}
/// `parent` is `node`'s parent, handed down by the walk (None at a body's
/// root): `Node::parent()` walks down from the tree's root on every call.
fn visit_for_calls_and_structure(&mut self, node: Node<'t>, parent: Option<Node<'t>>) {
stack_guard!();
let kind = node.kind();
self.maybe_capture_fn_refs(node);
// Rocket route macros: handler paths live in a raw token tree.
if kind == "macro_invocation" {
self.extract_rust_route_macro(node);
}
if kind == "call_expression" {
self.extract_call(node);
} else if kind == "struct_expression" {
self.extract_instantiation(node);
}
self.extract_static_member_ref(node, parent);
// Nested NAMED fns become their own nodes (a nested fn inside an impl
// method walks up to the impl and indexes as a METHOD).
if matches!(kind, "function_item" | "function_signature_item") {
let name = self.extract_name(node);
if name != "<anonymous>" {
self.extract_fn_or_method(node);
return;
}
}
// Structural nodes inside bodies.
if kind == "struct_item" {
self.extract_aggregate(node, "struct");
return;
}
if kind == "union_item" {
self.extract_aggregate(node, "union");
return;
}
if kind == "enum_item" {
self.extract_enum(node);
return;
}
if kind == "trait_item" {
self.extract_interface(node);
return;
}
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.visit_for_calls_and_structure(c, Some(node));
}
}
}
/// extractStaticMemberRef — the rust branch (#2328): an enum variant
/// written as a path (`Mode::A`, `mode::Mode::B`, a `Mode::C(x)` /
/// `Mode::D { .. }` pattern, `Self::A` in an impl) references the receiver
/// — the segment before the member, at the place it is written; the
/// resolver keeps it only on an enum that declares the member. A lowercase
/// receiver (module) or member (fn), a call's callee or a struct literal's
/// name (both already linked to their member), a path's prefix and a `use`
/// tree emit nothing. Mirrored byte-for-byte — change both.
fn extract_static_member_ref(&mut self, node: Node<'t>, parent: Option<Node<'t>>) {
let kind = node.kind();
if (kind != "scoped_identifier" && kind != "scoped_type_identifier") || self.stack.is_empty() {
return;
}
// Looked up only at a body's root (a `const X: M = M::A;` value).
let Some(parent) = parent.or_else(|| node.parent()) else { return };
let member_path = if kind == "scoped_identifier" {
!matches!(
parent.kind(),
"scoped_identifier"
| "scoped_type_identifier"
| "use_declaration"
| "use_list"
| "scoped_use_list"
| "use_as_clause"
| "use_wildcard"
)
} else {
parent.kind() == "struct_pattern"
};
if !member_path {
return;
}
if parent.kind() == "call_expression"
&& parent.child_by_field_name("function").map(|f| f.start_byte()) == Some(node.start_byte())
{
return;
}
let Some(member) = node.child_by_field_name("name") else { return };
let mut recv = node.child_by_field_name("path");
if let Some(r) = recv {
if r.kind() == "scoped_identifier" {
recv = r.child_by_field_name("name");
}
}
let Some(recv) = recv else { return };
if recv.kind() != "identifier" || !self.text(member).starts_with(|c: char| c.is_ascii_uppercase()) {
return;
}
let mut text = self.text(recv).to_string();
if text == "Self" {
text = self.receiver_type_of(node).unwrap_or_default();
}
if capitalized_re().is_match(&text) {
let owner = self.top_row();
self.push_ref_at(owner, &text, edge_kind_index("references").unwrap(), recv);
}
}
// --- fn refs (RUST_SPEC) ----------------------------------------------------
/// maybeCaptureFnRefs with RUST_SPEC's dispatch: arguments→args,
/// assignment_expression→rhs(right), field_initializer→value(value),
/// array_expression→list, static_item/let_declaration→varinit(value).
/// No layers/unwrap/special — only bare identifiers qualify (`&handler`
/// captures nothing). QUIRK: const_item is NOT in the dispatch.
fn maybe_capture_fn_refs(&mut self, node: Node<'t>) {
enum Mode {
Args,
Rhs,
Value,
List,
Varinit,
}
let mode = match node.kind() {
"arguments" => Mode::Args,
"assignment_expression" => Mode::Rhs,
"field_initializer" => Mode::Value,
"array_expression" => Mode::List,
"static_item" | "let_declaration" => Mode::Varinit,
_ => return,
};
if self.stack.is_empty() {
return;
}
let from = self.top_row();
let mut values: Vec<Node> = Vec::new();
match mode {
Mode::Args | Mode::List => {
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
values.push(c);
}
}
}
Mode::Rhs => {
if let Some(rhs) = node.child_by_field_name("right") {
// Param-storage skip: `o.cb = cb`.
let lhs_text = node
.child_by_field_name("left")
.map(|l| self.text(l))
.unwrap_or("");
let lhs_last = util::lhs_last_name()
.captures(lhs_text)
.and_then(|c| c.get(1))
.map(|m| m.as_str());
if !(lhs_last.is_some() && lhs_last == Some(self.text(rhs).trim())) {
values.push(rhs);
}
}
}
Mode::Value => {
let v = node.child_by_field_name("value").or_else(|| {
if node.named_child_count() > 0 {
node.named_child(node.named_child_count() - 1)
} else {
None
}
});
if let Some(v) = v {
values.push(v);
}
}
Mode::Varinit => {
// Destructuring skip: a tuple/struct pattern LHS extracts data,
// never a function alias (static_item's name is an identifier,
// let_declaration's `pattern` field can be a pattern).
let name_node = node
.child_by_field_name("name")
.or_else(|| node.child_by_field_name("pattern"));
if let Some(nn) = name_node {
if matches!(
nn.kind(),
"object_pattern" | "array_pattern" | "tuple_pattern" | "struct_pattern"
) {
return;
}
}
if let Some(v) = node.child_by_field_name("value") {
values.push(v);
}
}
}
for v in values {
// normalizeValue: idTypes = {identifier} only, no layers/unwrap.
if v.kind() == "identifier" {
let name = self.text(v).to_string();
if name.is_empty() || is_stoplisted(&name) {
continue;
}
let p = v.start_position();
self.fn_ref_cands.push(Cand {
from,
name,
line: p.row as u32 + 1,
column_byte: v.start_byte(),
row: p.row,
});
}
}
}
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
stack_guard!();
if depth > 12 {
return;
}
// Subtrees the body walker has already been through.
if walked.contains(&node.id()) {
return;
}
if depth > 0
&& matches!(
node.kind(),
"function_item" | "function_signature_item" | "arrow_function"
| "function_expression" | "lambda_literal" | "lambda_expression"
)
{
return;
}
self.maybe_capture_fn_refs(node);
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.scan_fn_ref_subtree(c, depth + 1, walked);
}
}
}
fn flush_fn_ref_candidates(&mut self) {
let cands = std::mem::take(&mut self.fn_ref_cands);
if cands.is_empty() || util::is_generated_file(self.file_path) {
return;
}
let mut seen: HashSet<(String, String)> = HashSet::new();
for c in cands {
if !c.name.starts_with("this.")
&& !c.name.contains("::")
&& !self.defined_fn_names.contains(&c.name)
&& !self.imported_names.contains(&c.name)
{
continue;
}
if !seen.insert((self.node_ids[c.from as usize].clone(), c.name.clone())) {
continue;
}
let column = util::col16(self.src, &self.line_starts, c.row, c.column_byte);
let name_ref = self.arena.put(&c.name);
self.tables.push_ref(&RefRow {
from_idx: c.from,
kind: FUNCTION_REF_CODE,
line: c.line,
column,
reference_name: name_ref,
candidates: NONE_STR,
from_id_str: NONE_STR,
});
}
}
// --- value refs -------------------------------------------------------------
fn flush_value_refs(&mut self, root: Node<'t>) {
let scopes = std::mem::take(&mut self.value_scopes);
let mut targets = std::mem::take(&mut self.fs_values);
let counts = std::mem::take(&mut self.fs_value_counts);
if std::env::var("CODEGRAPH_VALUE_REFS").as_deref() == Ok("0") {
return;
}
if targets.is_empty() || scopes.is_empty() || util::is_generated_file(self.file_path) {
return;
}
// Shadow prune — rust declarator shapes: const_item/static_item (name
// field) and let_declaration (the shadow source: `pattern` field; a
// tuple pattern bumps every named child).
let mut decl_counts: HashMap<&str, u32> = HashMap::new();
let mut bump = |decl_counts: &mut HashMap<&'t str, u32>, name_node: Option<Node<'t>>, src: &'t str, targets: &HashMap<String, u32>| {
if let Some(n) = name_node {
if matches!(n.kind(), "identifier" | "simple_identifier") {
let nm = &src[n.byte_range()];
if targets.contains_key(nm) {
*decl_counts.entry(nm).or_insert(0) += 1;
}
}
}
};
let mut dstack: Vec<Node> = vec![root];
let mut dvisited = 0usize;
while let Some(n) = dstack.pop() {
if dvisited >= MAX_VALUE_REF_NODES {
break;
}
dvisited += 1;
match n.kind() {
"const_item" | "static_item" => {
bump(&mut decl_counts, n.child_by_field_name("name"), self.src, &targets)
}
"let_declaration" => {
let left = n
.child_by_field_name("left")
.or_else(|| n.child_by_field_name("pattern"))
.or_else(|| n.named_child(0));
if let Some(left) = left {
if left.kind() == "identifier" {
bump(&mut decl_counts, Some(left), self.src, &targets);
} else {
for i in 0..left.named_child_count() {
bump(&mut decl_counts, left.named_child(i), self.src, &targets);
}
}
}
}
_ => {}
}
for i in 0..n.named_child_count() {
if let Some(c) = n.named_child(i) {
dstack.push(c);
}
}
}
let shadowed: Vec<String> = decl_counts
.iter()
.filter(|(nm, c)| **c > counts.get(**nm).copied().unwrap_or(1))
.map(|(nm, _)| nm.to_string())
.collect();
for nm in shadowed {
targets.remove(&nm);
}
if targets.is_empty() {
return;
}
let refs_kind = edge_kind_index("references").unwrap();
for scope in &scopes {
let mut seen: HashSet<&str> = HashSet::new();
let mut stack: Vec<Node> = vec![scope.node];
let mut visited = 0usize;
while let Some(n) = stack.pop() {
if visited >= MAX_VALUE_REF_NODES {
break;
}
visited += 1;
if matches!(n.kind(), "identifier" | "constant" | "name" | "simple_identifier") {
let ref_name = self.text(n);
if let Some(&target_row) = targets.get(ref_name) {
let target_id = self.node_ids[target_row as usize].as_str();
if target_id != self.node_ids[scope.row as usize]
&& ref_name != scope.name
&& !seen.contains(&target_id)
{
seen.insert(target_id);
let meta = self.arena.put(r#"{"valueRef":true}"#);
self.tables.push_edge(&EdgeRow {
source_idx: scope.row,
target_idx: target_row,
kind: refs_kind,
provenance: 0,
line: NONE,
column: NONE,
metadata_json: meta,
source_id_str: NONE_STR,
target_id_str: NONE_STR,
});
}
}
}
for i in 0..n.named_child_count() {
if let Some(c) = n.named_child(i) {
stack.push(c);
}
}
}
}
}
}
fn is_stoplisted(name: &str) -> bool {
matches!(
name,
"this" | "self" | "super" | "null" | "nil" | "true" | "false" | "undefined" | "new"
| "NULL" | "nullptr" | "None"
)
}
/// The function a turbofish wraps: `s.get::<u32>` → `s.get`,
/// `helper::<u8>` → `helper`; any other callee as is.
fn unwrap_generic_function(func: Node<'_>) -> Node<'_> {
if func.kind() == "generic_function" {
func.child_by_field_name("function").unwrap_or(func)
} else {
func
}
}
/// 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
/// TreeSitterExtractor: both emit the same bytes.
fn strip_turbofish(text: &str) -> String {
if !text.contains("::<") {
return text.to_string();
}
let chars: Vec<char> = text.chars().collect();
let mut out = String::with_capacity(text.len());
let mut i = 0;
while i < chars.len() {
if chars[i] == ':' && i + 2 < chars.len() && chars[i + 1] == ':' && chars[i + 2] == '<' {
let mut depth = 0i32;
let mut j = i + 2;
while j < chars.len() {
let c = chars[j];
if c == '<' {
depth += 1;
} else if c == '>' && chars[j - 1] != '-' {
depth -= 1;
if depth == 0 {
break;
}
}
j += 1;
}
i = j + 1;
continue;
}
out.push(chars[i]);
i += 1;
}
out
}
/// LITERAL_RECEIVER_TYPES (shared table).
fn is_literal_receiver(kind: &str) -> bool {
matches!(
kind,
"string" | "string_literal" | "interpreted_string_literal" | "raw_string_literal"
| "template_string" | "concatenated_string" | "formatted_string" | "f_string"
| "line_string_literal" | "string_content" | "heredoc_body"
| "number" | "number_literal" | "integer" | "integer_literal" | "float"
| "float_literal" | "int_literal" | "decimal_integer_literal" | "real_literal"
| "char_literal" | "character_literal" | "rune_literal" | "regex" | "regex_literal"
| "true" | "false" | "boolean_literal" | "bool_literal" | "none" | "null" | "nil"
| "null_literal" | "undefined"
| "list" | "list_literal" | "array" | "array_literal" | "array_creation_expression"
| "dictionary" | "dict_literal" | "object" | "tuple" | "set"
)
}
/// BUILTIN_TYPES (shared table — port the WHOLE set: a rust `String`
/// type_identifier IS suppressed via the Scala row).
fn is_builtin_type(name: &str) -> bool {
matches!(
name,
"string" | "number" | "boolean" | "void" | "null" | "undefined" | "never" | "any"
| "unknown" | "object" | "symbol" | "bigint" | "true" | "false"
| "str" | "bool" | "i8" | "i16" | "i32" | "i64" | "i128" | "isize"
| "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "f32" | "f64" | "char"
| "int" | "long" | "short" | "byte" | "float" | "double"
| "int8" | "int16" | "int32" | "int64" | "uint8" | "uint16" | "uint32" | "uint64"
| "float32" | "float64" | "complex64" | "complex128" | "rune" | "error"
| "Int" | "Long" | "Short" | "Byte" | "Float" | "Double" | "Boolean" | "Char"
| "Unit" | "String" | "Any" | "AnyRef" | "AnyVal" | "Nothing" | "Null"
)
}
fn opt_str(arena: &mut Arena, s: Option<&str>) -> StrRef {
match s {
Some(s) => arena.put(s),
None => NONE_STR,
}
}