fix(extract): emit Java generic parent relationships (#1510)

A generic parent — `class Foo extends Bar<T>` or `implements List<T>` — 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<T>` -> `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<T>` 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) <noreply@anthropic.com>
This commit is contained in:
oleksii-tumanov
2026-06-28 10:31:57 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent 940cb53f4d
commit 1f3f1c1ca6
2 changed files with 42 additions and 6 deletions
+20 -6
View File
@@ -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)
+22
View File
@@ -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<T> {}\n"
"interface Handler<T> {}\n"
"interface DerivedHandler extends Handler<Event> {}\n"
"class Service extends Base<Dependency> implements Handler<Event> {}\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")