Skip to content

Java: fields become nodes, via a new LanguageConfig.value_types - #3099

Open
ehrlichandreas wants to merge 2 commits into
Graphify-Labs:v8from
ehrlichandreas:java-value-nodes
Open

Java: fields become nodes, via a new LanguageConfig.value_types#3099
ehrlichandreas wants to merge 2 commits into
Graphify-Labs:v8from
ehrlichandreas:java-value-nodes

Conversation

@ehrlichandreas

Copy link
Copy Markdown

Builds on #3098 - please merge that one first. This branch contains its
commit; the diff below is only the Java part.

Problem

Same gap as #3098, one layer up. The shared engine dispatches on
classes, functions, imports and calls:

class_types=frozenset({"class_declaration", "interface_declaration", ...}),
function_types=frozenset({"method_declaration", "constructor_declaration"}),
import_types=frozenset({"import_declaration"}),
call_types=frozenset({"method_invocation", ...}),

A Java constant is a field_declaration and none of those. It never
becomes a node, and reading one is not recorded as a relation.

Measured on a 14-file Java project using five constants each read in
exactly one method: none of the five was a node, while all five
reading methods were. graphify query answered 1/5.

Change

LanguageConfig gains value_types and value_kind. Both default to
empty, so every other language behaves exactly as before and opts in
with one line when it wants to. Only _JAVA_CONFIG sets them here, to
field_declaration, because Java is the language this was measured on.

The engine then emits what the Go extractor already does after #3098: a
node per declared name marked with value_kind, and a references edge
with context value_use for a read inside a method body. Names not
declared in the file being extracted go to raw_value_refs and are
bound by _bind_cross_file_value_refs, which is language-agnostic and
comes from #3098.

One detail worth flagging for review: the value branch deliberately does
not return. A field_declaration also carries its type, which the
field-type-reference pass and the Java receiver-type table read from the
same subtree. Returning there broke eight Java tests that have nothing
to do with values, and that is how I found it.

Result

answered graph
before 1/5 100 nodes, 181 edges
after 5/5 134 nodes, 282 edges

Go is unchanged at 15/15 and TypeScript at 5/10 (its own separate issue:
9 of its 10 names are already nodes, so the loss there is in traversal,
not extraction).

Tests

tests/test_java_value_nodes.py, five cases. Three fail without this
change. One of them pins the opt-in from the other side: a Python module
constant must stay out of the graph, because Python does not set
value_types.

Full suite: 15 pre-existing failures before and after (missing optional
tree-sitter grammars), no new ones. ruff check clean on the touched
files.

Scope

Java only, on purpose. Of the 30 extractors, three mention a constant
node type at all, so the same gap likely exists in most of them - Rust
has const_item and static_item and neither is handled. Now that the
mechanism is config-driven, each language is one line plus its own
measurement, which I would rather do per language than in one sweep.

ehrlichandreas and others added 2 commits August 26, 2026 01:07
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>
Same gap as the Go one in the previous commit, in the shared engine
rather than a bespoke extractor. The dispatch knew classes, functions,
imports and calls. A Java constant is a field_declaration and none of
those, so it never became a node and reading one was not recorded as a
relation.

Measured on a 14-file Java project using five constants each read in
exactly one method: none of the five was a node, while all five reading
methods were. `graphify query` answered 1/5; after this change, 5/5.

LanguageConfig gains value_types and value_kind. Both default to empty,
so every other language behaves exactly as before and opts in with one
line when it wants to. Only _JAVA_CONFIG sets them here
(field_declaration), because that is the one language this was measured
on.

The engine emits the same two things the Go extractor does: a node per
declared name, marked value_kind, and a `references` edge with context
`value_use` for a read inside a method body. Names not declared in the
file go to raw_value_refs and are bound by
_bind_cross_file_value_refs, which is language-agnostic and already in
place from the Go commit.

The value branch deliberately does not return. A field_declaration also
carries its type, which the field-type-reference pass and the Java
receiver-type table read from the same subtree; returning there broke
eight Java tests that have nothing to do with values.

Full suite: 15 pre-existing failures before and after (missing optional
tree-sitter grammars), no new ones. tests/test_java_value_nodes.py adds
five cases, three of which fail without this change, including one that
pins the opt-in: a Python module constant must stay out of the graph.

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