From 5ee27e4d3fb5e04f7befcfc804cd5ff1f980f24f Mon Sep 17 00:00:00 2001 From: shazeb Date: Mon, 20 Jul 2026 23:26:50 +0530 Subject: [PATCH 1/2] fix(scala): walk type_definition right-hand sides for type references (#2049) type_definition (plain aliases, opaque type, match types) sits at the same template_body level as val_definition/var_definition, but the Scala dispatch only fired for the latter, so alias right-hand sides produced zero references edges. Two changes: - widen the dispatch to include type_definition; the RHS lives under the same 'type' field a val/var annotation uses, so the existing walk is reused as-is - teach _scala_collect_type_refs to recurse through match_type and type_case_clause nodes, which its whitelist did not know about, so match-type case patterns and results resolve too Fixture gains a TypeAliases class covering all three shapes; four new tests assert Map/HttpClient (alias), Long (opaque), and Option/ListBuffer (match type cases) each get references edges. --- graphify/extractors/engine.py | 8 ++++++-- tests/fixtures/sample.scala | 9 +++++++++ tests/test_languages.py | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 723606e64..589e2c85f 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -969,7 +969,8 @@ def _scala_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple _scala_collect_type_refs(arg, source, True, out) return if t in ("compound_type", "infix_type", "function_type", "tuple_type", - "annotated_type", "projected_type"): + "annotated_type", "projected_type", "match_type", + "type_case_clause"): for c in node.children: if c.is_named: _scala_collect_type_refs(c, source, generic, out) @@ -3075,8 +3076,11 @@ def _emit_java_parent_type(type_node, rel: str, at_line: int) -> None: return if (config.ts_module == "tree_sitter_scala" - and t in ("val_definition", "var_definition") + and t in ("val_definition", "var_definition", "type_definition") and parent_class_nid): + # type_definition covers plain aliases (`type Alias = List[Int]`), + # `opaque type`, and match types — the right-hand side sits under the + # same `type` field a val/var annotation uses, so the walk is shared. type_node = node.child_by_field_name("type") if type_node is not None: line = node.start_point[0] + 1 diff --git a/tests/fixtures/sample.scala b/tests/fixtures/sample.scala index 8a35888d8..4c1867e71 100644 --- a/tests/fixtures/sample.scala +++ b/tests/fixtures/sample.scala @@ -27,3 +27,12 @@ object HttpClientFactory { new HttpClient(Config(baseUrl, 30)) } } + +class TypeAliases { + type Routes = Map[String, HttpClient] + opaque type ClientId = Long + type Unwrap[X] = X match { + case Option[t] => t + case ListBuffer[t] => t + } +} diff --git a/tests/test_languages.py b/tests/test_languages.py index aff689d2a..85dd3eaaf 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -765,6 +765,24 @@ def test_scala_method_return_type_context(): assert ("create", "HttpClient") in _edge_labels(r, "references", "return_type") +def test_scala_type_alias_rhs_references(): + r = extract_scala(FIXTURES / "sample.scala") + assert ("TypeAliases", "Map") in _edge_labels(r, "references", "field") + assert ("TypeAliases", "HttpClient") in _edge_labels(r, "references", "generic_arg") + + +def test_scala_opaque_type_rhs_references(): + r = extract_scala(FIXTURES / "sample.scala") + assert ("TypeAliases", "Long") in _edge_labels(r, "references", "field") + + +def test_scala_match_type_case_references(): + r = extract_scala(FIXTURES / "sample.scala") + labels = _edge_labels(r, "references", "field") + assert ("TypeAliases", "Option") in labels + assert ("TypeAliases", "ListBuffer") in labels + + def test_scala_call_edges_have_call_context(): r = extract_scala(FIXTURES / "sample.scala") call_edges = _edges_with_relation(r, "calls") From 1a4d371d071f9c55b9d1cea2463ccca08b345a0c Mon Sep 17 00:00:00 2001 From: shazeb Date: Tue, 21 Jul 2026 08:05:16 +0530 Subject: [PATCH 2/2] docs(scala): move type_definition comment above the dispatch condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review on #2066 — no logic change. --- graphify/extractors/engine.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 589e2c85f..7a2470f21 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -3075,12 +3075,12 @@ def _emit_java_parent_type(type_node, rel: str, at_line: int) -> None: type_table[prop_name] = prop_type return + # type_definition covers plain aliases (`type Alias = List[Int]`), + # `opaque type`, and match types — the right-hand side sits under the + # same `type` field a val/var annotation uses, so the walk is shared. if (config.ts_module == "tree_sitter_scala" and t in ("val_definition", "var_definition", "type_definition") and parent_class_nid): - # type_definition covers plain aliases (`type Alias = List[Int]`), - # `opaque type`, and match types — the right-hand side sits under the - # same `type` field a val/var annotation uses, so the walk is shared. type_node = node.child_by_field_name("type") if type_node is not None: line = node.start_point[0] + 1