From 03ba8cc9a34206c5ae9df753379093d141dcb2e7 Mon Sep 17 00:00:00 2001 From: nothariharan Date: Tue, 29 Sep 2026 08:04:59 +0530 Subject: [PATCH] fix(kotlin): keep annotated inferred properties in the graph An annotation on a class property with no explicit type never assigned `line`, so the annotation loop raised UnboundLocalError and _safe_extract dropped the whole file. Set `line` before the type-node check so the file and its attribute edge both survive. Fixes #3884 Co-Authored-By: Grok 4.7 --- graphify/extractors/engine.py | 6 ++++- tests/test_kotlin_grammar.py | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index d3be0684a..609f7acbf 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -4855,9 +4855,13 @@ def _extract_generic( # Field-type references stay class-gated: top-level properties keep # their pre-#2565 (no-references) behavior unchanged. if parent_class_nid: + # #3884: the annotation loop below also reads `line`. An annotated + # property with an inferred type has no type node, so assigning + # `line` only inside that branch raised UnboundLocalError and + # _safe_extract dropped the whole file. + line = node.start_point[0] + 1 type_node = _kotlin_property_type_node(node) if type_node is not None: - line = node.start_point[0] + 1 refs: list[tuple[str, str]] = [] _kotlin_collect_type_refs(type_node, source, False, refs) for ref_name, role in refs: diff --git a/tests/test_kotlin_grammar.py b/tests/test_kotlin_grammar.py index 9b4869bc7..d6b71defb 100644 --- a/tests/test_kotlin_grammar.py +++ b/tests/test_kotlin_grammar.py @@ -657,3 +657,52 @@ def test_kotlin_object_member_call_survives_incremental_rebuild(tmp_path): assert _rebuild_code(corpus, changed_paths=[app], no_cluster=True, acquire_lock=False) is True assert _resolves(), "call stays resolved after an incremental rebuild" + + +def test_kotlin_annotated_inferred_property_keeps_the_file(tmp_path): + """#3884: `@Volatile var counter = 0` has an annotation and no explicit type. + + `line` used to be assigned only when a type node existed, so the annotation + loop raised UnboundLocalError and _safe_extract dropped every node in the file. + """ + r = _extract(tmp_path, { + "Repro.kt": ( + "class Repro {\n" + " @Volatile\n" + " var counter = 0\n" + "\n" + " fun ping() { }\n" + "}\n" + ), + }) + assert r["failed_sources"] == [], "the file must not be skipped" + repro = _find(r, "Repro") + ping = _find(r, ".ping()") + volatile = _find(r, "Volatile") + attr = [e for e in r["edges"] + if e["relation"] == "references" + and e["source"] == repro + and e["target"] == volatile + and e.get("context") == "attribute"] + assert attr, "the annotation is a references edge on the class" + assert any(e["relation"] == "method" and e["source"] == repro and e["target"] == ping + for e in r["edges"]), "a sibling method in the same file must survive" + + +def test_kotlin_annotated_explicit_type_property_still_references(tmp_path): + """Control for #3884: an annotation plus an explicit type already worked.""" + r = _extract(tmp_path, { + "Typed.kt": ( + "class Typed {\n" + " @Volatile\n" + " var counter: Int = 0\n" + "}\n" + ), + }) + typed = _find(r, "Typed") + volatile = _find(r, "Volatile") + assert any(e["relation"] == "references" + and e["source"] == typed + and e["target"] == volatile + and e.get("context") == "attribute" + for e in r["edges"])