From 64a6093376bab300860bcc4a66d7d6095f317f90 Mon Sep 17 00:00:00 2001 From: hibinm <110960284+hibinm@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:13:44 +1000 Subject: [PATCH] fix(groovy): emit inherits/implements edges for extends/implements tree-sitter-groovy exposes inheritance via the same `superclass` and `interfaces`/`type_list` fields as tree-sitter-java, but the inheritance- emitting block in `_extract_generic` was gated on `ts_module == "tree_sitter_java"`. Groovy was the only class-based JVM language in the file with no inheritance handler, so every Groovy `extends`/`implements` was silently dropped (contains/methods/imports/calls were unaffected). Widen the gate to include `tree_sitter_groovy`; the existing `_emit_java_parent_type` path handles the identical node shapes verbatim. Adds a base class + interface to the Groovy fixture and two regression tests (extends -> inherits, implements -> implements). Co-Authored-By: Claude Opus 4.8 (1M context) --- graphify/extract.py | 2 +- tests/fixtures/sample.groovy | 14 ++++++++++++++ tests/test_languages.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/graphify/extract.py b/graphify/extract.py index 0fdba70..f0a4927 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -3513,7 +3513,7 @@ def _extract_generic( context="generic_arg", metadata=metadata) # Java-specific: extends (superclass) / implements (interfaces) / interface-extends - if config.ts_module == "tree_sitter_java": + if config.ts_module in ("tree_sitter_java", "tree_sitter_groovy"): def _emit_java_parent(base_name: str, rel: str, at_line: int) -> None: if not base_name: return diff --git a/tests/fixtures/sample.groovy b/tests/fixtures/sample.groovy index 9a080ec..2e8bddc 100644 --- a/tests/fixtures/sample.groovy +++ b/tests/fixtures/sample.groovy @@ -19,3 +19,17 @@ class SampleService { processor.reset() } } + +interface Resettable { + void reset() +} + +class ExtendedService extends SampleService implements Resettable { + ExtendedService(Processor processor) { + super(processor) + } + + void reset() { + // no-op + } +} diff --git a/tests/test_languages.py b/tests/test_languages.py index 82fd1d6..b41c105 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -2195,6 +2195,35 @@ def test_groovy_no_dangling_edges(): assert e["source"] in node_ids +def test_groovy_extends_edge(): + """`class X extends Base` must emit an inherits edge. + + tree-sitter-groovy exposes inheritance via the same `superclass` field as + tree-sitter-java, but the inheritance handler was gated to Java only, so + Groovy extends/implements were silently dropped. + """ + r = extract_groovy(FIXTURES / "sample.groovy") + node_by_id = {n["id"]: n["label"] for n in r["nodes"]} + found = any( + "ExtendedService" in node_by_id.get(e["source"], "") + and "SampleService" in node_by_id.get(e["target"], "") + for e in r["edges"] if e["relation"] == "inherits" + ) + assert found, "ExtendedService should have inherits edge to SampleService" + + +def test_groovy_implements_edge(): + """`class X implements Iface` must emit an implements edge.""" + r = extract_groovy(FIXTURES / "sample.groovy") + node_by_id = {n["id"]: n["label"] for n in r["nodes"]} + found = any( + "ExtendedService" in node_by_id.get(e["source"], "") + and "Resettable" in node_by_id.get(e["target"], "") + for e in r["edges"] if e["relation"] == "implements" + ) + assert found, "ExtendedService should have implements edge to Resettable" + + def test_groovy_spock_finds_class(): r = extract_groovy(FIXTURES / "sample_spock.groovy") assert any("SampleSpec" in l for l in _labels(r))