From 1f3f1c1ca68f5dd15befbae247fc07552c592be4 Mon Sep 17 00:00:00 2001 From: oleksii-tumanov Date: Sun, 28 Jun 2026 10:31:57 +0100 Subject: [PATCH] fix(extract): emit Java generic parent relationships (#1510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A generic parent — `class Foo extends Bar` or `implements List` — is a `generic_type` node in the tree, but the inheritance extractor only fired on a bare `type_identifier`, so those inherits/implements edges were silently dropped. The parent type is now unwrapped to its base (`Bar` -> `Bar`) for the inherits/implements edge, and the type arguments are emitted as `generic_arg` references. Ported from PR #1511 by @oleksii-tumanov. Known pre-existing limitation (not introduced here, worth a separate follow-up): the extractor has no type-parameter tracking, so a bare parameter like `T` in `extends Container` still produces a `generic_arg` reference to `T`; the inherits/implements edge itself always targets the real base type, never `T`. Co-Authored-By: Claude Opus 4.8 (1M context) --- graphify/extract.py | 26 ++++++++++++++++++++------ tests/test_languages.py | 22 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index a3f9b291..863ae480 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -2780,11 +2780,25 @@ def _extract_generic( seen_ids.add(base_nid) add_edge(class_nid, base_nid, rel, at_line) + def _emit_java_parent_type(type_node, rel: str, at_line: int) -> None: + refs: list[tuple[str, str]] = [] + _java_collect_type_refs(type_node, source, False, refs) + parent_emitted = False + for ref_name, role in refs: + if role == "type" and not parent_emitted: + _emit_java_parent(ref_name, rel, at_line) + parent_emitted = True + elif role == "generic_arg": + target_nid = ensure_named_node(ref_name, at_line) + if target_nid != class_nid: + add_edge(class_nid, target_nid, "references", at_line, + context="generic_arg") + sup = node.child_by_field_name("superclass") if sup is not None: for sub in sup.children: - if sub.type == "type_identifier": - _emit_java_parent(_read_text(sub, source), "inherits", line) + if sub.is_named: + _emit_java_parent_type(sub, "inherits", line) break ifs = node.child_by_field_name("interfaces") @@ -2792,8 +2806,8 @@ def _extract_generic( for sub in ifs.children: if sub.type == "type_list": for tid in sub.children: - if tid.type == "type_identifier": - _emit_java_parent(_read_text(tid, source), "implements", line) + if tid.is_named: + _emit_java_parent_type(tid, "implements", line) if t == "interface_declaration": for child in node.children: @@ -2801,8 +2815,8 @@ def _extract_generic( for sub in child.children: if sub.type == "type_list": for tid in sub.children: - if tid.type == "type_identifier": - _emit_java_parent(_read_text(tid, source), "inherits", line) + if tid.is_named: + _emit_java_parent_type(tid, "inherits", line) for anno_name in _java_annotation_names(node, source): target_nid = ensure_named_node(anno_name, line) diff --git a/tests/test_languages.py b/tests/test_languages.py index ba411562..65ac204e 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -357,6 +357,28 @@ def test_java_normalizes_inherits_and_implements(): assert ("DataProcessor", "Processor") in _edge_labels(result, "implements") +def test_java_generic_parents_include_type_argument_references(tmp_path): + source = tmp_path / "GenericParents.java" + source.write_text( + "class Dependency {}\n" + "interface Event {}\n" + "class Base {}\n" + "interface Handler {}\n" + "interface DerivedHandler extends Handler {}\n" + "class Service extends Base implements Handler {}\n" + ) + + result = extract_java(source) + + assert ("Service", "Base") in _edge_labels(result, "inherits") + assert ("Service", "Handler") in _edge_labels(result, "implements") + refs = _edge_labels(result, "references", "generic_arg") + assert ("Service", "Dependency") in refs + assert ("Service", "Event") in refs + assert ("DerivedHandler", "Handler") in _edge_labels(result, "inherits") + assert ("DerivedHandler", "Event") in refs + + def test_java_parameter_return_generic_and_attribute_contexts(): result = extract_java(FIXTURES / "sample.java") assert ("build", "HttpClient") in _edge_labels(result, "references", "parameter_type")