diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 723606e64..e34ad8f80 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -2559,6 +2559,39 @@ def _php_emit_base(base_name: str, rel: str, at_line: int) -> None: add_edge(class_nid, target, "references", line, context="generic_arg") + # Kotlin-specific: primary_constructor class_parameters carry + # constructor-injected fields (`private val x: Foo` - the idiomatic + # Hilt/DI pattern). Mirrors the Scala class_parameters handling + # below; without this, every constructor-injected dependency was + # invisible to the graph (only body-level property_declaration + # vals were captured, and only delegation_specifiers for supertypes). + if config.ts_module == "tree_sitter_kotlin": + for child in node.children: + if child.type != "primary_constructor": + continue + for pc in child.children: + if pc.type != "class_parameters": + continue + for cp in pc.children: + if cp.type != "class_parameter": + continue + type_node = next( + (t for t in cp.children + if t.type in ("user_type", "nullable_type", "type_reference")), + None, + ) + if type_node is None: + continue + cp_line = cp.start_point[0] + 1 + refs: list[tuple[str, str]] = [] + _kotlin_collect_type_refs(type_node, source, False, refs) + for ref_name, role in refs: + ctx = "generic_arg" if role == "generic_arg" else "field" + target_nid = ensure_named_node(ref_name, cp_line) + if target_nid != class_nid: + add_edge(class_nid, target_nid, "references", + cp_line, context=ctx) + # Ruby: `class Dog < Animal` puts the base class in the `superclass` # field (a `<` token followed by a constant or scope_resolution). # There was no Ruby branch, so every Ruby inherits edge was dropped. diff --git a/tests/test_languages.py b/tests/test_languages.py index aff689d2a..82e619a46 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -711,6 +711,20 @@ def test_kotlin_user_types_still_emit_references(): assert ("run", "DataProcessor") in _edge_labels(r, "references", "parameter_type") +def test_kotlin_primary_constructor_val_emits_references(): + # `class HttpClient(private val config: Config)` - a constructor-injected + # field declared directly in the primary constructor's class_parameters, + # as opposed to a body-level `val`/`var` (property_declaration, already + # covered by test_kotlin_user_types_still_emit_references above). This is + # the idiomatic Kotlin/Hilt DI pattern (`@Inject constructor(private val + # x: Foo)`) and was previously dropped entirely: the Kotlin branch only + # walked delegation_specifiers (supertypes) and property_declaration + # (body vals), never primary_constructor/class_parameters, so every + # constructor-injected dependency was invisible to the graph. + r = extract_kotlin(FIXTURES / "sample.kt") + assert ("HttpClient", "Config") in _edge_labels(r, "references", "field") + + # ── Scala ───────────────────────────────────────────────────────────────────── def test_scala_no_error():