fix(extract): emit Java record component references (#1519)

A Java record's component types produced no edges, so a record's data dependencies
were invisible. The record_declaration handler now emits `references` edges from
each component type (field context; generic_arg for type arguments), skipping
primitives and — via #1518's in-scope type-parameter filtering — the record's own
type parameters. Handles standard, generic, primitive, and varargs components.

Ported from PR #1520 by @oleksii-tumanov (clean 3-way onto v8; builds on #1518).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
oleksii-tumanov
2026-06-28 19:55:51 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent 35fb437365
commit 981e2b93cf
2 changed files with 66 additions and 0 deletions
+28
View File
@@ -2863,6 +2863,34 @@ def _extract_generic(
add_edge(class_nid, target_nid, "references", line,
context="attribute")
if t == "record_declaration":
components = node.child_by_field_name("parameters")
if components is not None:
for component in components.children:
if component.type == "formal_parameter":
type_node = component.child_by_field_name("type")
elif component.type == "spread_parameter":
type_node = next(
(
child
for child in component.children
if child.is_named
and child.type not in ("modifiers", "variable_declarator")
),
None,
)
else:
continue
refs: list[tuple[str, str]] = []
_java_collect_type_refs(type_node, source, False, refs)
component_line = component.start_point[0] + 1
for ref_name, role in refs:
ctx = "generic_arg" if role == "generic_arg" else "field"
target_nid = ensure_named_node(ref_name, component_line)
if target_nid != class_nid:
add_edge(class_nid, target_nid, "references",
component_line, context=ctx)
# Scala: extends_clause carries `extends Base with Trait1 with Trait2`.
# The first base after `extends` is `inherits`; each subsequent
# type after `with` is `mixes_in`. Also walk class_parameters for
+38
View File
@@ -434,6 +434,44 @@ def test_java_field_type_references_have_field_context(tmp_path):
)
def test_java_record_component_type_references(tmp_path):
source = tmp_path / "RecordComponents.java"
source.write_text(
"class Payload {}\n"
"class Item {}\n"
"class Attachment {}\n"
"record Order(Payload payload, List<Item> items, int count, "
"Attachment... attachments) {}\n"
)
result = extract_java(source)
assert ("Order", "Payload") in _edge_labels(result, "references", "field")
assert ("Order", "List") in _edge_labels(result, "references", "field")
assert ("Order", "Item") in _edge_labels(result, "references", "generic_arg")
assert ("Order", "Attachment") in _edge_labels(result, "references", "field")
def test_java_record_components_skip_type_parameters(tmp_path):
source = tmp_path / "GenericRecord.java"
source.write_text(
"class Payload {}\n"
"class Box<X> {}\n"
"record Batch<T>(T value, Box<T> boxed, Box<Payload> retained) {}\n"
)
result = extract_java(source)
assert ("Batch", "T") not in _edge_labels(result, "references")
assert not [
node
for node in result["nodes"]
if node.get("label") == "T" and not node.get("source_file")
]
assert ("Batch", "Box") in _edge_labels(result, "references", "field")
assert ("Batch", "Payload") in _edge_labels(result, "references", "generic_arg")
def test_java_type_annotations_have_attribute_context(tmp_path):
source = tmp_path / "TypeAnnotations.java"
source.write_text(