Skip to content

Go: package-level constants and variables become nodes - #3098

Open
ehrlichandreas wants to merge 1 commit into
Graphify-Labs:v8from
ehrlichandreas:go-value-nodes
Open

Go: package-level constants and variables become nodes#3098
ehrlichandreas wants to merge 1 commit into
Graphify-Labs:v8from
ehrlichandreas:go-value-nodes

Conversation

@ehrlichandreas

Copy link
Copy Markdown

Problem

The Go extractor dispatches on four node types:

if t == "function_declaration": ...
if t == "method_declaration":   ...
if t == "type_declaration":     ...
if t == "import_declaration":   ...

Go's grammar also has const_declaration and var_declaration. A
package-level constant therefore never becomes a node, and reading one
is not recorded as a relation.

I hit this while comparing retrieval tools on a 59-file Go project,
using fifteen constants that are each read in exactly one function.
Every query returned the same thing:

$ graphify query "sandboxProviderFD"
No matching nodes found.

Checking the graph rather than guessing: none of the fifteen
constants was a node, while all fifteen of the reading functions
were. The graph held the answer and offered no way in.

Change

Three parts, roughly 130 lines.

  1. const_declaration and var_declaration become nodes, marked with
    value_kind, with a contains edge from their file.

  2. Reading such a name inside a function body emits a references edge
    with context value_use. Without it the node exists but stays
    unreachable. Restricted to names declared as package-level values, so
    a local identifier produces nothing.

  3. Names not declared in the file being extracted go to a new
    raw_value_refs bucket, bound afterwards by
    _bind_cross_file_value_refs. This mirrors raw_calls: an extractor
    sees one file and reports what it cannot resolve rather than
    guessing. Three of the fifteen constants were read from a sibling
    file and stayed unreachable until this pass existed. Bound only for
    an unambiguous name, the same god-node guard the call resolver uses.
    raw_value_refs[].caller_nid is rewritten by both id_remap and
    sym_remap, as raw_calls[].caller_nid already is.

Result

Same project, same fifteen questions, graphify query --budget 2000:

answered graph build time
before 0/15 327 nodes, 797 edges 0.76 s
after 15/15 395 nodes, 976 edges 0.76 s

The jump-style questions this tool is built for are unaffected: 7/7
before and after on a separate 7-question set.

Tests

tests/test_go_value_nodes.py, six cases covering both directions: a
declared constant becomes a node, an unused one gets no reader, a
cross-file read is bound, and a local variable neither becomes a node
nor binds to one. Four of the six fail without this change.

Full suite: 25 failures before, 25 after (all pre-existing, in
test_terraform.py and test_skillgen.py), 4728 passing. ruff check
clean on the touched files.

Note on scope

This is Go only. Of the 30 extractors, three mention a constant node
type at all, so the same gap likely exists elsewhere - Rust has
const_item and static_item and neither is handled. I did not touch
those: the cross-file pass is language-agnostic, so another extractor
only needs to emit value_kind nodes and raw_value_refs to use it.

The Go extractor dispatched on four node types: function_declaration,
method_declaration, type_declaration and import_declaration. Go's
grammar also has const_declaration and var_declaration, so a
package-level constant never became a node, and reading one was not
recorded as a relation.

Measured on a 59-file Go project, asking for fifteen constants each read
in exactly one function: none of the fifteen constants was a node, while
all fifteen of the reading functions were. `graphify query <constant>`
answered "No matching nodes found" every time. The graph held the answer
and offered no way in.

Three parts:

- const_declaration and var_declaration become nodes, marked with
  value_kind so later passes can find them, with a `contains` edge from
  the file.
- Reading such a name inside a function body emits a `references` edge
  with context `value_use`. Without it the node exists but stays
  unreachable. Restricted to names declared as package-level values, so
  a local identifier produces nothing.
- Names not declared in the file being extracted go to a new
  raw_value_refs bucket, bound afterwards by
  _bind_cross_file_value_refs. This mirrors raw_calls: an extractor sees
  one file and reports what it cannot resolve rather than guessing. Of
  the fifteen constants above, three were read from a sibling file, and
  those three stayed unreachable until this pass existed. Bound only for
  an unambiguous name, the same god-node guard the call resolver uses.
  raw_value_refs[].caller_nid is rewritten by both id_remap and
  sym_remap, as raw_calls[].caller_nid already is - left stale the edge
  would dangle on its source.

After the change the same fifteen questions are answered 15/15. The
graph grows from 327 to 395 nodes and 797 to 976 edges on that project,
with no measurable change in build time (0.76 s both ways).

tests/test_go_value_nodes.py covers both directions: a declared constant
becomes a node, an unused one gets no reader, a cross-file read is
bound, and a local variable neither becomes a node nor binds to one.
Four of the six fail without this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant