mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
fix(python): walk a module-level assignment's initializer scoped to the name (#693 for Python)
The assignment branch minted the node and stopped, so every call on the
right-hand side was missing from the graph:
APP = compute() # no caller
handler = lambda: target() # no caller
MAPPING = {"a": compute()} # no caller
first, second = compute(), f() # no caller, and no symbol either
That is everything a module wires up at import time — `app = FastAPI()`,
`ENGINE = create_engine(url)`, `router = build_router()`, handler registries —
so whatever those build looked unreferenced.
Fix: walk the `right` field through visitFunctionBody with the assigned name
pushed on the scope stack. A tuple target mints no symbol, so its RHS is walked
at the enclosing scope rather than lost. Gated to Python — Ruby shares this
branch and gets its own turn.
Class attributes are untouched: they never reach this branch (the dispatcher's
class-scope gate excludes them), so their calls keep riding the class node.
Giving them symbols of their own is a separate question — there are no nodes
for them today at all.
A function-as-value in an initializer (`REGISTRY = {"org": Serializer}`) now
produces a reference from BOTH the assigned name and the file node, because the
dispatcher's own scan runs either way. That is the shape Go's #744 already
ships — verified against the Go arm on the same input — so the two languages
stay consistent; the function-ref test's expectation is updated to match.
Measured on three Python trees (libresdr, ADRC-betaflight,
bot_predlogka_big_project — 799 files): 0 lost refs, 0 re-attributed, +3057
new; node and edge counts unchanged. Kernel parity holds — 6221/6221 files
byte-identical across the sweep.
This commit is contained in:
@@ -469,14 +469,37 @@ impl<'t> Walker<'t> {
|
||||
let docstring = preceding_docstring(node, self.src);
|
||||
let left = node.child_by_field_name("left").or_else(|| node.named_child(0));
|
||||
let right = node.child_by_field_name("right").or_else(|| node.named_child(1));
|
||||
let Some(left) = left else { return };
|
||||
if !matches!(left.kind(), "identifier" | "constant") {
|
||||
return;
|
||||
let mut assigned: Option<(u32, String)> = None;
|
||||
if let Some(left) = left {
|
||||
if matches!(left.kind(), "identifier" | "constant") {
|
||||
let name = self.text(left).to_string();
|
||||
let signature = right.map(|r| util::init_signature(self.text(r)));
|
||||
// No isConst hook ⇒ always `variable` (UPPER_CASE constants included).
|
||||
let row = self.create_node(
|
||||
"variable",
|
||||
&name,
|
||||
node,
|
||||
Extra { docstring, signature, ..Extra::default() },
|
||||
);
|
||||
if let Some(row) = row {
|
||||
assigned = Some((row, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Walk the initializer ATTRIBUTED to the assigned name (#693): a
|
||||
// module-level `app = FastAPI()` / `handler = lambda: run()` dropped
|
||||
// every call on the right-hand side. A tuple target mints no symbol, so
|
||||
// its RHS is walked at the enclosing scope rather than lost.
|
||||
if let Some(right) = right {
|
||||
match assigned {
|
||||
Some((row, name)) => {
|
||||
self.stack.push(Scope { row, kind: "variable", name });
|
||||
self.visit_function_body(right);
|
||||
self.stack.pop();
|
||||
}
|
||||
None => self.visit_function_body(right),
|
||||
}
|
||||
}
|
||||
let name = self.text(left).to_string();
|
||||
let signature = right.map(|r| util::init_signature(self.text(r)));
|
||||
// No isConst hook ⇒ always `variable` (UPPER_CASE constants included).
|
||||
self.create_node("variable", &name, node, Extra { docstring, signature, ..Extra::default() });
|
||||
}
|
||||
|
||||
fn extract_import(&mut self, node: Node<'t>) {
|
||||
|
||||
Reference in New Issue
Block a user