Merge pull request #1710 from benrfairless/fix/ruby-namespaced-constructor-callee

fix(extraction): reduce A::B.new callee to the constant
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-29 03:32:26 +01:00
commit 006101ae4f
2 files changed
+57 -1

No files matched your search

+5 -1
View File
@@ -2249,11 +2249,15 @@ static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, C
// method is `new`. The constructor body lives in `initialize`, so a callee
// of "new" never resolves. Redirect to the receiver type name so the call
// links to the class/constructor like every other language's `new T()`.
// Scope-resolved receivers (`Admin::User.new`) get the same treatment —
// the callee carries the full constant path ("Admin::User"), whose bare
// leaf ("User") is what resolution joins on.
if (lang == CBM_LANG_RUBY) {
TSNode m = ts_node_child_by_field_name(node, TS_FIELD("method"));
TSNode recv = ts_node_child_by_field_name(node, TS_FIELD("receiver"));
if (!ts_node_is_null(m) && !ts_node_is_null(recv) &&
strcmp(ts_node_type(recv), "constant") == 0) {
(strcmp(ts_node_type(recv), "constant") == 0 ||
strcmp(ts_node_type(recv), "scope_resolution") == 0)) {
char *mt = cbm_node_text(a, m, source);
if (mt && strcmp(mt, "new") == 0) {
char *rt = cbm_node_text(a, recv, source);
+52
View File
@@ -857,6 +857,57 @@ TEST(ruby_module) {
PASS();
}
/* A Ruby `Const.new` call is rewritten to name the CONSTANT, not the method:
* the constructor body lives in `initialize`, so the call should link the class
* like every other language's `new T()`. The rewrite fired only for bare
* `constant` receivers, so a scope-resolved receiver kept the whole call text
* as its callee ("Admin::User.new"), which resolves to nothing and cannot join
* the Ruby LSP's constructor rows, whose callee is the class QN.
*
* Both spellings must land on the bare constant path; a retained ".new" suffix
* is the bug's signature. */
TEST(ruby_constructor_callee_is_constant_path) {
CBMFileResult *r = extract("module Admin\n class User\n end\nend\n"
"class Widget\nend\n"
"class Maker\n"
" def build\n"
" Admin::User.new\n"
" Widget.new\n"
" end\n"
"end\n",
CBM_LANG_RUBY, "t", "maker.rb");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
int scoped = 0, bare = 0, kept_new = 0;
for (int i = 0; i < r->calls.count; i++) {
const char *callee = r->calls.items[i].callee_name;
if (strcmp(callee, "Admin::User") == 0)
scoped = 1;
if (strcmp(callee, "Widget") == 0)
bare = 1;
/* Either failure mode: the method name survived on its own, or the
* whole `Recv.new` expression did. */
size_t n = strlen(callee);
if (strcmp(callee, "new") == 0 || (n >= 4 && strcmp(callee + n - 4, ".new") == 0))
kept_new = 1;
}
if (!scoped || !bare || kept_new) {
printf(" ruby callees (%d):\n", r->calls.count);
for (int i = 0; i < r->calls.count; i++)
printf(" '%s'\n", r->calls.items[i].callee_name);
}
/* Scope-resolved receiver reduces to the constant path; its bare leaf is
* what resolution joins on. */
ASSERT(scoped);
/* Bare receiver: pre-existing behaviour, guarded against regression. */
ASSERT(bare);
ASSERT_FALSE(kept_new);
cbm_free_result(r);
PASS();
}
/* --- C# --- */
TEST(csharp_class) {
CBMFileResult *r = extract("namespace App { public class Service { public void Run() {} public "
@@ -13493,6 +13544,7 @@ SUITE(extraction) {
RUN_TEST(php_function);
RUN_TEST(ruby_class);
RUN_TEST(ruby_module);
RUN_TEST(ruby_constructor_callee_is_constant_path);
RUN_TEST(csharp_class);
RUN_TEST(csharp_interface);
RUN_TEST(csharp_extension_block_members);