From 9b49bfd9eb0ada46022676a4f3da18e57f615446 Mon Sep 17 00:00:00 2001 From: oleksii-tumanov Date: Sat, 27 Jun 2026 10:03:46 +0100 Subject: [PATCH] fix(extract): emit references for Java type annotations (#1487) Annotations on Java classes/interfaces/records (e.g. @Service, @Entity) produced no edge, so type-level framework wiring was invisible even though method-level annotations were already captured. The class/interface/record handler now emits `references` edges with the existing `attribute` context, reusing the renamed `_java_annotation_names` helper (was `_java_method_annotation_names`; logic unchanged) at both the type and method call sites. Ported from PR #1487 by @oleksii-tumanov. Resolved an additive test conflict with #1485 by keeping both new test functions. Co-Authored-By: Claude Opus 4.8 (1M context) --- graphify/extract.py | 14 ++++++++++---- tests/test_languages.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index cf076d82..5a80bf42 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -684,11 +684,11 @@ def _java_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[ _java_collect_type_refs(c, source, generic, out) -def _java_method_annotation_names(method_node, source: bytes) -> list[str]: - """Collect annotation names from a Java method's `modifiers` child.""" +def _java_annotation_names(declaration_node, source: bytes) -> list[str]: + """Collect annotation names from a Java declaration's `modifiers` child.""" names: list[str] = [] modifiers = None - for child in method_node.children: + for child in declaration_node.children: if child.type == "modifiers": modifiers = child break @@ -2792,6 +2792,12 @@ def _extract_generic( if tid.type == "type_identifier": _emit_java_parent(_read_text(tid, source), "inherits", line) + for anno_name in _java_annotation_names(node, source): + target_nid = ensure_named_node(anno_name, line) + if target_nid != class_nid: + add_edge(class_nid, target_nid, "references", line, + context="attribute") + # 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 @@ -3206,7 +3212,7 @@ def _extract_generic( target_nid = ensure_named_node(ref_name, line) if target_nid != func_nid: add_edge(func_nid, target_nid, "references", line, context=ctx) - for anno_name in _java_method_annotation_names(node, source): + for anno_name in _java_annotation_names(node, source): target_nid = ensure_named_node(anno_name, line) if target_nid != func_nid: add_edge(func_nid, target_nid, "references", line, context="attribute") diff --git a/tests/test_languages.py b/tests/test_languages.py index 7cda26ce..d16353e2 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -363,6 +363,21 @@ def test_java_field_type_references_have_field_context(tmp_path): ) +def test_java_type_annotations_have_attribute_context(tmp_path): + source = tmp_path / "TypeAnnotations.java" + source.write_text( + '@Service\n' + '@Entity(name = "checkout")\n' + 'class CheckoutService {}\n' + ) + + result = extract_java(source) + + refs = _edge_labels(result, "references", "attribute") + assert ("CheckoutService", "Service") in refs + assert ("CheckoutService", "Entity") in refs + + def test_csharp_field_type_references_have_field_context(): r = extract_csharp(FIXTURES / "sample.cs") refs = _references(r)