fix(rust): extract module-level static and const declarations

The Rust extractor gated every non-function item on struct_item,
enum_item and trait_item, so tree-sitter-rust's static_item and
const_item never produced a node: a constant existed in the graph only
through the files that referenced it, never from the Rust that defines
it (0 of 783 names in a jdx/mise snapshot).

Add a branch for both node types: a module-level declaration gets a
node with a file-level contains edge, an associated const inside an
impl is attributed to the impl like a method, and the declared type is
referenced the way a struct field type is.

Fixes #3471
This commit is contained in:
L4XB committed 2026-09-10 18:44:26 +02:00
1 parent adf814fd5e
commit 70ff464064
4 files changed
+65 -1

No files matched your search

+1
View File
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## 0.9.57 (unreleased)
- Fix: Rust module-level `static` and `const` declarations (and associated consts inside an `impl`) are now extracted as nodes with a `contains` edge and a reference to their declared type — neither node type had a branch, so a constant only ever reached the graph through files that referenced it (#3471, thanks @sortakool).
- Fix: an incremental rebuild no longer wipes cross-file project AST nodes — re-extracting one `.csproj`/`.sln` was dropping package/framework nodes of a *referenced* project (whose stub carried the referenced file's `source_file`); the AST-replacement set is now derived from the files actually extracted (#3411, thanks @hopstreax).
- Fix: when duplicate nodes merge, the richer (more complete) node is now kept as the survivor and the losers' non-empty fields are folded in, instead of a shorter-id passing mention winning and dropping content (#3372, thanks @abhay-codes07).
- Fix: a C# generic call site with explicit type arguments — `Get<int>(...)`, unqualified or through `this` — now resolves to the method definition instead of capturing `Get<int>` as the callee and failing to match (#3406, thanks @abhay-codes07).
+30 -1
View File
@@ -59,7 +59,7 @@ _RUST_TRAIT_METHOD_BLOCKLIST: frozenset[str] = frozenset({
})
def extract_rust(path: Path) -> dict:
"""Extract functions, structs, enums, traits, impl methods, and use declarations from a .rs file."""
"""Extract functions, structs, enums, traits, impl methods, statics/consts, and use declarations from a .rs file."""
try:
import tree_sitter_rust as tsrust
from tree_sitter import Language, Parser
@@ -325,6 +325,35 @@ def extract_rust(path: Path) -> dict:
walk(child, parent_impl_nid=item_nid)
return
if t in ("static_item", "const_item"):
# `static NAME: T = …;` / `const NAME: T = …;` at module level, or an
# associated const inside an impl. Neither node type had a branch, so a
# constant reached the graph only through files that referenced it,
# never from the Rust that defines it (#3471).
name_node = node.child_by_field_name("name")
if name_node:
item_name = _read_text(name_node, source)
line = node.start_point[0] + 1
if parent_impl_nid:
item_nid = _make_id(parent_impl_nid, item_name)
add_node(item_nid, f".{item_name}", line)
add_edge(parent_impl_nid, item_nid, "contains", line)
else:
item_nid = _make_id(stem, item_name)
add_node(item_nid, item_name, line)
add_edge(file_nid, item_nid, "contains", line)
type_node = node.child_by_field_name("type")
if type_node is not None:
refs: list[tuple[str, str]] = []
_rust_collect_type_refs(type_node, source, False, refs)
for ref_name, role in refs:
tgt = ensure_named_node(ref_name, line)
if tgt == item_nid:
continue
ctx = "generic_arg" if role == "generic_arg" else "field"
add_edge(item_nid, tgt, "references", line, context=ctx)
return
if t == "impl_item":
type_node = node.child_by_field_name("type")
trait_node = node.child_by_field_name("trait")
+8
View File
@@ -1,10 +1,18 @@
use std::collections::HashMap;
pub static RETRY_LIMIT: usize = 3;
pub const DEFAULT_MODE: &str = "fast";
const NODE_LIMIT: Limit = Limit(3);
struct Limit(usize);
struct Graph {
nodes: HashMap<String, Vec<String>>,
}
impl Graph {
const CAPACITY: usize = 16;
fn new() -> Self {
Graph { nodes: HashMap::new() }
}
+26
View File
@@ -315,6 +315,32 @@ def test_rust_calls_are_extracted():
assert e["confidence"] == "EXTRACTED"
def test_rust_finds_static_and_const_items():
r = extract_rust(FIXTURES / "sample.rs")
by_id = {n["id"]: n["label"] for n in r["nodes"]}
labels = set(by_id.values())
for name in ("RETRY_LIMIT", "DEFAULT_MODE", "NODE_LIMIT"):
assert name in labels, name
assert any(
e["relation"] == "contains" and by_id.get(e["target"]) == name
for e in r["edges"]
), f"{name} has no file-level contains edge"
# An associated const is attributed to its impl, like a method.
assert ".CAPACITY" in labels
assert any(
e["relation"] == "contains"
and by_id.get(e["source"]) == "Graph"
and by_id.get(e["target"]) == ".CAPACITY"
for e in r["edges"]
)
# The declared type is referenced like a struct field type.
assert any(
e["relation"] == "references"
and by_id.get(e["source"]) == "NODE_LIMIT"
and by_id.get(e["target"]) == "Limit"
for e in r["edges"]
)
def test_rust_import_edges_have_import_context():
r = extract_rust(FIXTURES / "sample.rs")
import_edges = _edges_with_relation(r, "imports", "imports_from")