mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
fix(rust): walk a const/static initializer scoped to the declared symbol (#693 for Rust)
`const_item`/`static_item` ride extractVariable's generic fallback, which
minted the node and stopped — the initializer was never walked, so every call
inside it was missing from the graph:
const LEN: usize = compute_len(); // no caller
static REGISTRY: Lazy<Cfg> = Lazy::new(|| build_cfg()); // no caller
That second shape is how once_cell/lazy_static singletons, handler tables and
static configs are written, so whatever they build looked unreferenced.
Fix: walk the `value` field through visitFunctionBody with the declared symbol
pushed on the scope stack. The fallback is shared, so the walk is gated to
Rust — the other languages on it spell their initializer differently and get
their own turn. The declared symbol is identified by the `name` FIELD, so the
phantom node the fallback also mints for a bare-identifier initializer
(`const MAX: u32 = OTHER;` → `MAX` plus a spurious `OTHER`) is left exactly as
it is: a separate defect, deliberately not folded in.
Measured on three Rust projects (gnss_rust, emmc-reader-gui,
bot_predlogka_rust — 282 files): 0 lost refs, 0 re-attributed, +86 new; node
and edge counts unchanged. Kernel parity holds (226/226 non-deferred files
byte-identical).
This commit is contained in:
@@ -25,6 +25,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- On Windows, the Claude Code prompt hook written by `codegraph install` failed with "command not found" when hooks run through Git Bash, which needs the `.cmd` extension to find the launcher. The installer now writes the platform-correct command, and re-running `codegraph install` (or `codegraph upgrade`) repairs an existing install in place. (#1466)
|
||||
- Python classes used as values — `return SomeSerializer` from a factory method, `handler = SomeClass` aliases, registry dicts and lists, and classes passed as arguments — now produce reference edges in the graph. Previously these idioms were invisible, so on Django and Django REST Framework projects, asking for a serializer's callers or the impact of editing it missed the views that actually use it. Re-index after upgrading to pick up the new edges. (#1478)
|
||||
- When a file changed on disk after its last index sync, `codegraph_node` and `codegraph_explore` could return a different symbol's code under the requested name — current file bytes cut at outdated line positions — while presenting it as verbatim, trustworthy source. This hit hardest on projects queried through `projectPath` (for example, sub-projects of a monorepo), which have no live file watcher to flag pending edits. Both tools now verify each file against the index before showing sliced code: an out-of-date file is either shown whole with its full current source, or its code is withheld with a clear "changed on disk" notice — never served as a wrong slice. A fresh re-index restores normal output automatically. Thanks @inth3shadows for the thorough report and verification passes. (#1474)
|
||||
- Rust `const` and `static` initializers now contribute their calls: `static REGISTRY: Lazy<Cfg> = Lazy::new(|| build())`, `const LEN: usize = compute_len()`. Previously anything a lazily-built singleton or a computed constant called was missing from the graph entirely. Re-index with `codegraph index -f` after upgrading to pick up the new edges.
|
||||
- Scala `val`/`var` definitions now contribute the calls their initializer makes — `val handler = () => process(msg)`, `val client = buildClient()`, `lazy val engine = start()`. Previously everything on the right-hand side was dropped, which on a val-heavy codebase (SpinalHDL hardware descriptions, Akka wiring) is most of the wiring: a 32-file SpinalHDL project gained 812 references it had been missing. Re-index with `codegraph index -f` after upgrading to pick up the new edges.
|
||||
- TypeScript and JavaScript module-level declarations now name themselves as the caller of whatever their initializer runs. `const cfg = loadConfig()` recorded the *file* as loadConfig's caller, which is no use for callers or impact; it now records `cfg`. And an object literal that wasn't exported — `const handlers = { onSave: () => persist() }` — was skipped entirely, so nothing inside it reached the graph at all. Re-index with `codegraph index -f` after upgrading to pick up the new edges.
|
||||
- `codegraph_explore` again lists a dynamic-dispatch link when the same two symbols are also joined by an ordinary call, instead of dropping it from the summary.
|
||||
|
||||
@@ -1210,6 +1210,26 @@ impl Counter {
|
||||
);
|
||||
expect(implRefs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('walks a const/static initializer scoped to the declared symbol (#693 for Rust)', () => {
|
||||
// The declaration minted a node and stopped, so a handler table, a
|
||||
// lazily-built singleton or any computed const linked to nothing.
|
||||
const code = `
|
||||
const LEN: usize = compute_len();
|
||||
static REGISTRY: Lazy<Cfg> = Lazy::new(|| build_cfg());
|
||||
`;
|
||||
const result = extractFromSource('lib.rs', code);
|
||||
const byId = new Map(result.nodes.map((n) => [n.id, n]));
|
||||
const owner = (name: string) => {
|
||||
const u = result.unresolvedReferences.find(
|
||||
(r) => r.referenceKind === 'calls' && r.referenceName === name
|
||||
);
|
||||
const n = u ? byId.get(u.fromNodeId) : undefined;
|
||||
return n ? `${n.kind}:${n.name}` : undefined;
|
||||
};
|
||||
expect(owner('compute_len')).toBe('variable:LEN');
|
||||
expect(owner('build_cfg')).toBe('variable:REGISTRY');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java Extraction', () => {
|
||||
|
||||
@@ -209,3 +209,8 @@ fn mount() {
|
||||
}
|
||||
|
||||
routes![top_level_h];
|
||||
|
||||
// Initializer walks attributed to the declared symbol (#693).
|
||||
const INIT_CONST: usize = compute_len();
|
||||
static INIT_LAZY: Lazy<Cfg> = Lazy::new(|| build_cfg());
|
||||
static INIT_ALIAS: fn() = free_fn;
|
||||
|
||||
@@ -650,6 +650,8 @@ impl<'t> Walker<'t> {
|
||||
/// and the initializer value is never body-walked.
|
||||
fn extract_variable(&mut self, node: Node<'t>) {
|
||||
let docstring = preceding_docstring(node, self.src);
|
||||
let name_field = node.child_by_field_name("name");
|
||||
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" {
|
||||
@@ -657,7 +659,7 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
let name = self.text(child).to_string();
|
||||
if !name.is_empty() {
|
||||
self.create_node(
|
||||
let row = self.create_node(
|
||||
"variable",
|
||||
&name,
|
||||
child,
|
||||
@@ -667,6 +669,26 @@ impl<'t> Walker<'t> {
|
||||
..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.
|
||||
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();
|
||||
}
|
||||
None => self.visit_function_body(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2889,6 +2889,8 @@ export class TreeSitterExtractor {
|
||||
} else {
|
||||
// Generic fallback for other languages
|
||||
// Try to find identifier children
|
||||
const nameField = getChildByField(node, 'name');
|
||||
let declared: Node | null = null;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'identifier' || child?.type === 'variable_declarator') {
|
||||
@@ -2897,13 +2899,30 @@ export class TreeSitterExtractor {
|
||||
: extractName(child, this.source, this.extractor);
|
||||
|
||||
if (name && name !== '<anonymous>') {
|
||||
this.createNode(kind, name, child, {
|
||||
const created = this.createNode(kind, name, child, {
|
||||
docstring,
|
||||
isExported,
|
||||
});
|
||||
if (created && nameField && child.startIndex === nameField.startIndex) {
|
||||
declared = created;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Walk the initializer ATTRIBUTED to the declared symbol (#693). Rust
|
||||
// only for now: `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. The other languages sharing this fallback spell
|
||||
// their initializer differently and get their own turn.
|
||||
if (this.language === 'rust') {
|
||||
const valueNode = getChildByField(node, 'value');
|
||||
if (valueNode) {
|
||||
if (declared) this.nodeStack.push(declared.id);
|
||||
this.visitFunctionBody(valueNode, '');
|
||||
if (declared) this.nodeStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user