From a19b9e90ec174efed8114512c001a6ec9da6dcda Mon Sep 17 00:00:00 2001 From: hibinm <110960284+hibinm@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:17:32 +1000 Subject: [PATCH] fix(ruby): emit inherits edge for class superclass `class Dog < Animal` exposes the base in the `superclass` field, but the inheritance handler in `_extract_generic` had branches for java/kotlin/c#/scala/cpp/php/swift/python and none for Ruby, so every Ruby `inherits` edge was silently dropped (contains/methods/calls unaffected). Add a Ruby branch that reads the `superclass` field, handling both a bare `constant` (`< Animal`) and a `scope_resolution` (`< Foo::Bar` -> Bar). Adds a subclass to the Ruby fixture and a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) --- graphify/extract.py | 31 +++++++++++++++++++++++++++++++ tests/fixtures/sample.rb | 6 ++++++ tests/test_languages.py | 16 ++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/graphify/extract.py b/graphify/extract.py index 2c7b26f99..0fdba70c3 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -3426,6 +3426,37 @@ def _extract_generic( add_edge(class_nid, target, "references", line, context="generic_arg") + # 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. + if config.ts_module == "tree_sitter_ruby": + sup = node.child_by_field_name("superclass") + if sup is not None: + base = "" + for sub in sup.children: + if sub.type == "constant": + base = _read_text(sub, source) + break + if sub.type == "scope_resolution": + consts = [c for c in sub.children if c.type == "constant"] + if consts: + base = _read_text(consts[-1], source) + break + if base: + base_nid = _make_id(stem, base) + if base_nid not in seen_ids: + base_nid = _make_id(base) + if base_nid not in seen_ids: + nodes.append({ + "id": base_nid, + "label": base, + "file_type": "code", + "source_file": "", + "source_location": "", + }) + seen_ids.add(base_nid) + add_edge(class_nid, base_nid, "inherits", line) + # C#-specific: inheritance / interface implementation via base_list if config.ts_module == "tree_sitter_c_sharp": csharp_type_params = _csharp_type_parameters_in_scope(node, source) diff --git a/tests/fixtures/sample.rb b/tests/fixtures/sample.rb index 429bf78c7..cd2e4b028 100644 --- a/tests/fixtures/sample.rb +++ b/tests/fixtures/sample.rb @@ -22,6 +22,12 @@ class ApiClient end end +class TimeoutApiClient < ApiClient + def fetch(path, method) + super + end +end + def parse_response(raw) JSON.parse(raw) end diff --git a/tests/test_languages.py b/tests/test_languages.py index 3393ab164..82fd1d6e7 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -299,6 +299,22 @@ def test_ruby_finds_function(): assert any("parse_response" in l for l in _labels(r)) +def test_ruby_inherits_edge(): + """`class Sub < Base` must emit an inherits edge. + + Ruby exposes the base class in the `superclass` field, but there was no + Ruby branch in the inheritance handler, so the edge was silently dropped. + """ + r = extract_ruby(FIXTURES / "sample.rb") + node_by_id = {n["id"]: n["label"] for n in r["nodes"]} + found = any( + "TimeoutApiClient" in node_by_id.get(e["source"], "") + and node_by_id.get(e["target"], "") == "ApiClient" + for e in r["edges"] if e["relation"] == "inherits" + ) + assert found, "TimeoutApiClient should have inherits edge to ApiClient" + + # ── C# ─────────────────────────────────────────────────────────────────────── def test_csharp_no_error():