diff --git a/graphify/extract.py b/graphify/extract.py index df61693..61f8afb 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -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 diff --git a/tests/test_languages.py b/tests/test_languages.py index 5c7165e..c76933d 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -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 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 {}\n" + "record Batch(T value, Box boxed, Box 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(