fix(extraction): keep Ruby superclass names bare

tree-sitter-ruby wraps `class C < Base` in a `superclass` node whose
child is a `constant` (or `scope_resolution` for `A::B`).
collect_bases_from_field matched neither, so the superclass fell through
to the raw-text fallback and base_classes stored "< Base" — operator
included. A base spelled that way never resolves, so Ruby subclasses
produced zero INHERITS edges, and the Rails chain
(User < ApplicationRecord < ActiveRecord::Base) never linked up.

Accept both node kinds. The accompanying inherit_ruby rows fail 5/5
without this change, each on the "<" substring that is the bug's
signature.

Refs #1701

Assisted-by: OpenCode:anthropic/claude-opus-5
Signed-off-by: Ben Fairless <ben@oaf.org.au>
This commit is contained in:
Ben Fairless committed 2026-08-18 15:56:41 +08:00
1 parent 4d7d9f1117
commit 4b1cdf5780
2 files changed
+75 -1

No files matched your search

+7 -1
View File
@@ -2405,7 +2405,13 @@ static int collect_bases_from_field(CBMArena *a, TSNode field_node, const char *
* dotted base like `mod.Base`). Without these the raw-text fallback
* below captured the whole "(Base)" field text, which never
* resolved -> zero INHERITS edges for Python subclasses. */
strcmp(ck, "identifier") == 0 || strcmp(ck, "attribute") == 0) {
strcmp(ck, "identifier") == 0 || strcmp(ck, "attribute") == 0 ||
/* Ruby `class C < Base` wraps the base in a `superclass` node whose
* child is a `constant` (or `scope_resolution` for `A::B`). Without
* these the raw-text fallback captured "< Base" (operator included),
* which never resolves — breaking INHERITS and the Ruby LSP's
* superclass chain. */
strcmp(ck, "constant") == 0 || strcmp(ck, "scope_resolution") == 0) {
char *t = cbm_node_text(a, child, source);
if (t) {
char *angle = strchr(t, '<');
+68
View File
@@ -31,6 +31,10 @@
* C++ — extraction is correct (regression guards).
* Rust — impl_traits array (not base_classes) is the capture point; Rust
* rows verify impl_traits entries for `impl Trait for Struct`.
* Ruby — green as of the fix that accompanies these rows: the
* `superclass` wrapper's `constant` / `scope_resolution` child is
* now matched, so `class C < Base` no longer falls through to the
* raw-text fallback and stores "< Base".
*
* STRUCTURE
* ─────────
@@ -1658,6 +1662,69 @@ TEST(inherit_rust_impls) {
PASS();
}
/* ═══════════════════════════════════════════════════════════════════
* RUBY (expected: GREEN — regression guards for the fix in this PR)
*
* Root cause fixed here: tree-sitter-ruby wraps `class C < Base` in a
* `superclass` node whose child is a `constant` (or `scope_resolution`
* for `A::B`). collect_bases_from_field() matched neither, so the
* raw-text fallback stored the whole field text "< Base" — operator
* included — and a base name spelled that way never resolves, so Ruby
* subclasses produced zero INHERITS edges.
*
* Every row therefore also asserts "<" never appears inside a captured
* base name: that substring IS the bug's signature.
* ═══════════════════════════════════════════════════════════════════ */
static const inherit_case_t ruby_cases[] = {
/* ── single bare-constant superclass ────────────────────────── */
{CBM_LANG_RUBY,
"m.rb",
"class Animal\nend\n\nclass Dog < Animal\nend\n",
"Dog",
{"Animal", NULL},
{"<", NULL},
1},
/* ── scope-resolved superclass (A::B) ───────────────────────── */
{CBM_LANG_RUBY,
"m.rb",
"class ApplicationRecord < ActiveRecord::Base\nend\n",
"ApplicationRecord",
{"ActiveRecord::Base", NULL},
{"<", NULL},
1},
/* ── the Rails chain's second link (bare, resolves to the above) ─ */
{CBM_LANG_RUBY,
"m.rb",
"class ApplicationRecord < ActiveRecord::Base\nend\n"
"class User < ApplicationRecord\nend\n",
"User",
{"ApplicationRecord", NULL},
{"<", NULL},
1},
/* ── subclass declared inside a module body ─────────────────── */
{CBM_LANG_RUBY,
"m.rb",
"class Base\nend\n\nmodule Admin\n class Panel < Base\n end\nend\n",
"Panel",
{"Base", NULL},
{"<", NULL},
1},
/* ── exception subclass (stdlib superclass) ─────────────────── */
{CBM_LANG_RUBY,
"m.rb",
"class AppError < StandardError\nend\n",
"AppError",
{"StandardError", NULL},
{"<", NULL},
1},
};
TEST(inherit_ruby) {
RUN_CASES(ruby_cases);
PASS();
}
/* ═══════════════════════════════════════════════════════════════════
* SUITE declaration
* ═══════════════════════════════════════════════════════════════════ */
@@ -1668,6 +1735,7 @@ SUITE(extraction_inheritance) {
RUN_TEST(inherit_csharp);
RUN_TEST(inherit_cpp);
RUN_TEST(inherit_rust_impls);
RUN_TEST(inherit_ruby);
/* Languages expected RED (broken extractors — reproduce-first) */
RUN_TEST(inherit_python); /* RED: identifier-node not matched in collect_bases_from_field */